Merged
Conversation
ui/app/app.js
Outdated
| src: '/images/icon-128.png', | ||
| }), | ||
|
|
||
| // metamask namlterChangese |
SeungjunWe
pushed a commit
to DcentWallet/metamask-extension
that referenced
this pull request
Jul 13, 2020
(Fix) Display non-zero fiat value
yqrashawn
added a commit
to yqrashawn/conflux-portal
that referenced
this pull request
Aug 31, 2020
7 tasks
github-merge-queue bot
pushed a commit
that referenced
this pull request
Jan 29, 2026
Do not merge before this gets in MetaMask/core#7413 ## **Description** # Performance Comparison: Per-Chain Token Cache Storage This PR implements per-chain file storage for `tokensChainsCache` in `TokenListController`, replacing the single-file approach. Each chain's token list is now stored in a separate file via StorageService, reducing write amplification during incremental updates. --- ## 📊 Summary | Category | This PR | Main Branch | Improvement | |----------|---------|-------------|-------------| | Cold restart (getAllPersistedState) | **216.20ms** | **374.30ms** | **42% faster** | | Cold restart (cache load) | **~310ms** (parallel) | Included in state | ----- | | Onboarding - Token cache | **4.40MB** (StorageService) | **4.40MB** (in state) | Stored separately | | Onboarding - Background saves | **~23MB** each | **~28MB** each | **~5MB less per save** | | Onboarding - Token cache in saves | ❌ No | ✅ Yes | **Eliminated** | | **Add Monad** | **39KB** (new chain only) | **~4.4MB** (full cache rewritten) | **Only new chain** | | **Add Avalanche** | **157KB** (new chain only) | **~4.6MB** (full cache rewritten) | **Only new chain** | | **Background saves (idle)** | **~23MB** | **~28MB** | **~18% smaller** | | **TokenListController in state** | **0.04KB** | **4,601KB** | **Cache moved out** | **Key Results**: 1. **Adding a single chain on main branch** triggers a state save that rewrites all TokenListController cache (~4.6MB) plus all other controllers (~23MB) = ~28MB total. 2. **This PR writes ONLY the new chain** (e.g., 39KB for Monad) to StorageService. The full token cache is NOT rewritten. 3. **Background saves are ~5MB smaller** (~23MB vs ~28MB) because the token cache is stored separately. --- ## 🧪 Test Scenarios ### Scenario 1: Cold Restart (Existing User) **Setup**: Extension with 10 networks cached (8 popular + Monad + Avalanche), then browser restart. #### This PR **Controller Storage:** ``` [ControllerStorage PERF] getAllPersistedState complete - 216.20ms ``` **StorageService - getAllKeys:** ``` [StorageService PERF] getAllKeys TokenListController - 10 keys found - 277.60ms ``` **StorageService - Per-chain getItem (parallel reads):** | Chain | Size | Read Time | |-------|------|-----------| | 0x1 (Ethereum) | 2022.39KB | 80.40ms | | 0x38 (BSC) | 1045.19KB | 125.90ms | | 0x2105 (Base) | 436.73KB | 97.20ms | | 0x89 (Polygon) | 388.91KB | 128.30ms | | 0xa4b1 (Arbitrum) | 345.79KB | 159.50ms | | 0xa86a (Avalanche) | 156.90KB | 161.60ms | | 0xa (Optimism) | 125.76KB | 132.20ms | | 0xe708 (Linea) | 40.13KB | 163.20ms | | 0x8f (Monad) | 39.15KB | 131.80ms | | 0xaa36a7 (Sepolia) | 0.04KB | 162.70ms | **Total cache size: ~4.60MB across 10 chains** #### Main Branch **Cold restart with 10 networks cached (8 popular + Monad + Avalanche):** ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 374.30ms [ControllerStorage PERF] TokenListController read - 4601.13KB - 374.30ms ``` #### Comparison | Metric | This PR | Main Branch | Improvement | |--------|---------|-------------|-------------| | **getAllPersistedState** | **216.20ms** | **374.30ms** | **42% faster** | | TokenListController in state | **0.04KB** | **4,601KB** | Moved to StorageService | | StorageService cache load | **~163ms** (10 chains parallel) | N/A | Separate loading | | Total chains | 10 | 10 | Same | | Total state size | ~23MB | ~28MB | **~18% smaller** | **Key insight**: The main state loads **158ms faster** on this PR because it's ~5MB smaller. The token cache is loaded separately via StorageService in parallel during controller initialization. --- ### Scenario 2: Fresh Onboarding **Setup**: Fresh wallet creation, enable all popular networks, wait for token lists to fetch. #### This PR **Initial state (fresh install):** ``` [ControllerStorage PERF] getAllPersistedState complete - 13.70ms [StorageService PERF] getAllKeys TokenListController - 0 keys found - 23.50ms ``` **Per-chain writes as networks are enabled:** | Chain | Size | Stringify | Write | Total | |-------|------|-----------|-------|-------| | 0xaa36a7 (Sepolia) | 0.04KB | 0.00ms | 12.00ms | 12.00ms | | 0xe708 (Linea) | 40.13KB | 0.00ms | 81.70ms | 81.70ms | | 0xa (Optimism) | 125.76KB | 0.20ms | 81.20ms | 81.40ms | | 0x2105 (Base) | 436.73KB | 0.80ms | 79.10ms | 79.90ms | | 0x89 (Polygon) | 388.91KB | 0.80ms | 72.80ms | 73.60ms | | 0xa4b1 (Arbitrum) | 345.79KB | 0.80ms | 67.10ms | 67.90ms | | 0x1 (Ethereum) | 2022.39KB | 4.20ms | 68.40ms | 72.60ms | | 0x38 (BSC) | 1045.19KB | 1.90ms | 37.80ms | 39.70ms | **Summary:** - **Total data written**: ~4.40MB (8 individual writes) - **Number of writes**: 8 (one per chain) - **Total write time**: ~509ms (sum of individual writes) #### Main Branch **Initial state (fresh install):** ``` [ControllerStorage PERF] getAllPersistedState complete - 14.60ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9952.04KB - 129.10ms ``` **After clicking "All Popular Networks":** ``` [ControllerStorage PERF] set() - TokenListController size: 4405.07KB [ControllerStorage PERF] set() complete - 27830.24KB - 526.70ms ``` **Summary:** - **TokenListController size**: 4,405KB (~4.4MB) - cached in controller state - **Total state written**: 27,830KB (~27.2MB) - entire MetaMask state - **Write time**: 526.70ms #### Comparison | Metric | This PR | Main Branch | Difference | |--------|---------|-------------|------------| | Token cache data | **4.40MB** | **4.40MB** | Same amount | | Token cache location | **StorageService** (separate) | **Main state** | Separated | | Background save size | **~23MB** | **~28MB** | **~5MB smaller** | | Token cache in every save | ❌ **No** | ✅ **Yes** | **Eliminated** | **Key insight**: Both branches have continuous background saves. On main branch, every save includes the ~4.4MB token cache. On this PR, the token cache is stored separately via StorageService, making each background save ~5MB smaller. --- ### Scenario 3: Add New Chain **Setup**: Existing wallet with cached networks, add a new network. #### This PR **Avalanche (0xa86a)**: ``` [StorageService PERF] setItem TokenListController:tokensChainsCache:0xa86a - 156.90KB - stringify: 0.80ms, write: 137.20ms, total: 138.00ms ``` **Monad (0x8f)**: ``` [StorageService PERF] setItem TokenListController:tokensChainsCache:0x8f - 39.15KB - stringify: 0.00ms, write: 2.40ms, total: 2.40ms ``` **zkSync Era (0x144)**: ``` [StorageService PERF] setItem TokenListController:tokensChainsCache:0x144 - 12.98KB - stringify: 0.00ms, write: 0.90ms, total: 0.90ms ``` **Polygon (0x89)**: ``` [StorageService PERF] setItem TokenListController:tokensChainsCache:0x89 - 388.91KB - stringify: 0.90ms, write: 15.40ms, total: 16.30ms ``` #### Main Branch **Adding Monad (0x8f) to existing cache:** ``` [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.62KB - 569.00ms ``` **Adding Avalanche (0xa86a) to existing cache:** ``` [ControllerStorage PERF] set() - TokenListController size: 4601.13KB [ControllerStorage PERF] set() complete - 28036.55KB - 559.30ms ``` **Note**: Each chain addition triggers a full state save that includes ALL TokenListController cache (~4.6MB) plus ALL other controllers (~23MB) = ~28MB total. #### Comparison **Token cache write for new chain:** | Chain | This PR | Main Branch | Difference | |-------|---------|-------------|------------| | **Monad (39KB)** | 39KB to StorageService | Full ~4.4MB cache rewritten | **Only new chain written** | | **Avalanche (157KB)** | 157KB to StorageService | Full ~4.6MB cache rewritten | **Only new chain written** | **Total state save triggered:** | Metric | This PR | Main Branch | Difference | |--------|---------|-------------|------------| | State size | ~23MB (no cache) | ~28MB (with cache) | **~5MB smaller** | | Token cache included | ❌ No | ✅ Yes | **Separated** | | New chain write | **39-157KB** (separate file) | Included in 28MB | **Isolated** | --- ## 📋 Raw Logs ### This PR - Cold Restart ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 216.20ms [StorageService PERF] getAllKeys TokenListController - 10 keys found - 277.60ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0x1 - 2022.39KB - read: 80.40ms, total: 80.40ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0x2105 - 436.73KB - read: 97.20ms, total: 97.20ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0x38 - 1045.19KB - read: 125.80ms, total: 125.90ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0x89 - 388.91KB - read: 128.30ms, total: 128.30ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0x8f - 39.15KB - read: 131.80ms, total: 131.80ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0xa - 125.76KB - read: 132.20ms, total: 132.20ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0xa4b1 - 345.79KB - read: 159.50ms, total: 159.50ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0xa86a - 156.90KB - read: 161.60ms, total: 161.60ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0xaa36a7 - 0.04KB - read: 162.70ms, total: 162.70ms [StorageService PERF] getItem TokenListController:tokensChainsCache:0xe708 - 40.13KB - read: 163.20ms, total: 163.20ms ``` ### This PR - Onboarding ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 13.70ms [StorageService PERF] getAllKeys TokenListController - 0 keys found - 23.50ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0xaa36a7 - 0.04KB - stringify: 0.00ms, write: 12.00ms, total: 12.00ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0xe708 - 40.13KB - stringify: 0.00ms, write: 81.70ms, total: 81.70ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0xa - 125.76KB - stringify: 0.20ms, write: 81.20ms, total: 81.40ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0x2105 - 436.73KB - stringify: 0.80ms, write: 79.10ms, total: 79.90ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0x89 - 388.91KB - stringify: 0.80ms, write: 72.80ms, total: 73.60ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0xa4b1 - 345.79KB - stringify: 0.80ms, write: 67.10ms, total: 67.90ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0x1 - 2022.39KB - stringify: 4.20ms, write: 68.40ms, total: 72.60ms [StorageService PERF] setItem TokenListController:tokensChainsCache:0x38 - 1045.19KB - stringify: 1.90ms, write: 37.80ms, total: 39.70ms ``` ### This PR - Add New Chain ``` # Adding Monad (39KB) [StorageService PERF] setItem TokenListController:tokensChainsCache:0x8f - 39.15KB - stringify: 0.10ms, write: 1.60ms, total: 1.70ms [ControllerStorage PERF] set() - TokenListController size: 0.04KB [ControllerStorage PERF] set() complete - 23433.75KB - 418.90ms # Adding Avalanche (157KB) [StorageService PERF] setItem TokenListController:tokensChainsCache:0xa86a - 156.90KB - stringify: 0.50ms, write: 5.30ms, total: 5.80ms [ControllerStorage PERF] set() - TokenListController size: 0.04KB [ControllerStorage PERF] set() complete - 23435.80KB - 416.10ms ``` ### Main Branch - Cold Restart ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 374.30ms [ControllerStorage PERF] TokenListController read - 4601.13KB - 374.30ms [ControllerStorage PERF] set() - TokenListController size: 4601.13KB [ControllerStorage PERF] set() complete - 28033.23KB - 426.60ms [ControllerStorage PERF] set() - TokenListController size: 4601.13KB [ControllerStorage PERF] set() complete - 28033.01KB - 564.00ms ``` ### Main Branch - Onboarding ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 14.60ms [ControllerStorage PERF] set() complete - 0.06KB - 0.30ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9952.04KB - 129.10ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9952.30KB - 130.20ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9952.67KB - 136.10ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9952.97KB - 136.50ms [ControllerStorage PERF] set() - TokenListController size: 0.06KB [ControllerStorage PERF] set() complete - 9955.72KB - 142.20ms [ControllerStorage PERF] set() - TokenListController size: 4405.07KB [ControllerStorage PERF] set() complete - 27830.24KB - 526.70ms ``` ### Main Branch - Add New Chain (Monad + Avalanche) ``` # Adding Monad (39KB) [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.62KB - 569.00ms # Adding Avalanche (157KB) [ControllerStorage PERF] set() - TokenListController size: 4601.13KB [ControllerStorage PERF] set() complete - 28036.55KB - 559.30ms ``` --- ## 🔧 How Performance Was Measured Performance logging was added to: 1. **BrowserStorageAdapter** (`app/scripts/lib/stores/browser-storage-adapter.ts`) - Logs for `getItem`, `setItem`, `getAllKeys` operations - Measures read time, stringify time, write time, and data size 2. **ExtensionStore** (`app/scripts/lib/stores/extension-store.ts`) - Logs for `getAllPersistedState` and controller state writes - Measures TokenListController-specific read/write performance To enable logging, set `PERF_LOGGING_ENABLED = true` in both files. --- ## 📝 Logging Code Reference (Main Branch) The following code was added to `extension-store.ts` on main branch to capture performance metrics: ### Helper Functions (add at top of file after imports) ```typescript // ============ PERF LOGGING (for testing) ============ const PERF_LOGGING_ENABLED = true; function getSizeKB(obj: unknown): string { try { const str = JSON.stringify(obj); return (str.length / 1024).toFixed(2); } catch { return 'N/A'; } } function logControllerReadPerf( controllerName: string, data: unknown, timeMs: number, ): void { if (!PERF_LOGGING_ENABLED) { return; } const sizeKB = getSizeKB(data); console.warn( `[ControllerStorage PERF] ${controllerName} read - ${sizeKB}KB - ${timeMs.toFixed(2)}ms`, ); } function logControllerWritePerf( controllerName: string, data: unknown, timeMs: number, ): void { if (!PERF_LOGGING_ENABLED) { return; } const sizeKB = getSizeKB(data); console.warn( `[ControllerStorage PERF] ${controllerName} write - ${sizeKB}KB - ${timeMs.toFixed(2)}ms`, ); } // ============ END PERF LOGGING ============ ``` ### In `get()` method - Add at start of method: ```typescript const perfStart = performance.now(); if (PERF_LOGGING_ENABLED) { console.warn('[ControllerStorage PERF] getAllPersistedState started'); } ``` ### In `get()` method - Add after data is loaded: ```typescript // PERF: Log overall time and TokenListController size if (PERF_LOGGING_ENABLED) { const elapsed = performance.now() - perfStart; console.warn( `[ControllerStorage PERF] getAllPersistedState complete - ${elapsed.toFixed(2)}ms`, ); // Log TokenListController state size specifically if (data.TokenListController) { logControllerReadPerf( 'TokenListController', data.TokenListController, elapsed, ); } } ``` ### In `set()` method - Add logging: ```typescript const perfStart = performance.now(); // PERF: Log TokenListController size before write if ( PERF_LOGGING_ENABLED && isObject(data) && hasProperty(data, 'TokenListController') ) { const tlcSize = getSizeKB(data.TokenListController); console.warn( `[ControllerStorage PERF] set() - TokenListController size: ${tlcSize}KB`, ); } // ... existing set logic ... // PERF: Log total write time (add after await local.set()) if (PERF_LOGGING_ENABLED) { const elapsed = performance.now() - perfStart; const totalSize = getSizeKB({ data, meta }); console.warn( `[ControllerStorage PERF] set() complete - ${totalSize}KB - ${elapsed.toFixed(2)}ms`, ); } ``` ### Expected Log Output **Cold Restart:** ``` [ControllerStorage PERF] getAllPersistedState started [ControllerStorage PERF] getAllPersistedState complete - 374.30ms [ControllerStorage PERF] TokenListController read - 4601.13KB - 374.30ms ``` **Write (adding chain or background save):** ``` [ControllerStorage PERF] set() - TokenListController size: 4601.13KB [ControllerStorage PERF] set() complete - 28036.55KB - 559.30ms ``` --- ## 💡 Key Takeaways 1. **Write amplification eliminated**: Adding a single chain now writes only that chain's data (~30-200KB) instead of the entire cache (~4MB) 2. **Faster incremental updates**: Per-chain writes are significantly faster than full cache rewrites 3. **Cold restart trade-off**: Parallel file reads + getAllKeys adds some overhead vs single file read, but the difference is minimal 4. **Onboarding improvement**: Total data written during onboarding is reduced by avoiding cumulative rewrites --- ## ✅ PR Branch: Background Writes No Longer Include Token Cache On this PR branch, background writes show: ``` [ControllerStorage PERF] set() - TokenListController size: 0.04KB ← TINY! No cache! [ControllerStorage PERF] set() complete - 23425.30KB - 481.60ms ← ~23MB (not 28MB) ``` **Key proof**: TokenListController is only 0.04KB in the main state because the ~4.4MB token cache is stored separately in StorageService. --- ##⚠️ Main Branch: Continuous Background Write Amplification During testing on main branch, we observed that the **entire 27.8MB state is being rewritten repeatedly** even when the user is idle: ``` [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.62KB - 632.30ms [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.62KB - 456.40ms [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.62KB - 606.40ms [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.59KB - 625.10ms [ControllerStorage PERF] set() - TokenListController size: 4444.22KB [ControllerStorage PERF] set() complete - 27877.59KB - 597.90ms ``` ### Why This Happens MetaMask has background processes that trigger state saves: - Token balance polling - Price updates - Network status checks - Account sync - DeFi positions updates - etc. Each time ANY controller state changes, the **entire state** (~27.8MB) is serialized and written to storage, including the **4.4MB TokenListController cache that hasn't changed**. ### Impact Comparison | Metric | This PR | Main Branch | |--------|---------|-------------| | State size per write | **~23MB** | **~28MB** | | TokenListController in state | **0.04KB** | **4,601KB** | | Token cache included in saves | ❌ No | ✅ Yes (every save) | | Write time | ~480ms | ~550ms | ### How This PR Helps By moving `tokensChainsCache` to StorageService: 1. **Background saves are ~18% smaller** (~23MB instead of ~28MB) 2. **Token cache only written when it actually changes** (new chain added or cache refresh) 3. **Reduced disk I/O** - ~5MB less data serialized and written on every background save 4. **Better SSD/storage health** - less unnecessary write cycles ## **Changelog** CHANGELOG entry: No user facing changes; this only updates the storage location for tokenListController. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes how `TokenListController` persists cached token lists and adds a state migration writing to `browser.storage.local`, which could impact startup/migration behavior if keys or storage operations fail. > > **Overview** > **Moves `TokenListController` token list caching to StorageService.** The controller messenger now allows `StorageService:*` actions and `TokenListControllerInit` fires `controller.initialize()` on startup to load cached lists from storage (logging errors but not failing init). > > **Adds migration #190 to preserve existing caches.** Migration `190` copies `tokensChainsCache` entries into per-chain `storageService:TokenListController:tokensChainsCache:{chainId}` keys without overwriting existing entries, then clears the in-state cache and bumps fixtures/snapshots to version `190`. > > **Updates tests and deps for the new storage behavior.** Jest mocks for `webextension-polyfill` are made async/shared across imports, e2e state persistence ignores StorageService-prefixed keys, and `@metamask/assets-controllers` is bumped to `^98.0.0`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 62be895. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: MetaMask Bot <metamaskbot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Got a nicely animating drop-down menu with animated sandwich icon implemented for our UX overhaul branch.
Merge at your pleasure @kumavis