├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .watchmanconfig ├── README.md ├── app ├── app.js ├── components │ ├── .gitkeep │ ├── current-route.js │ └── list-view.js ├── controllers │ ├── .gitkeep │ ├── application.js │ └── index.js ├── helpers │ ├── .gitkeep │ ├── capitalize.js │ └── format-name.js ├── index.html ├── instance-initializers │ └── link-tracking.js ├── mock-data.js ├── models │ ├── .gitkeep │ ├── update.js │ └── user.js ├── resolver.js ├── router.js ├── routes │ ├── .gitkeep │ ├── application.js │ └── profile.js ├── services │ └── shared-storage.js ├── styles │ └── app.css ├── templates │ ├── application.hbs │ ├── components │ │ ├── .gitkeep │ │ ├── current-route.hbs │ │ └── list-view.hbs │ ├── index.hbs │ └── profile.hbs └── utils │ └── component-debug.js ├── bower.json ├── config └── environment.js ├── ember-cli-build.js ├── exercises ├── exercise-0.md ├── exercise-1.md ├── exercise-2.md ├── exercise-3.md ├── exercise-4.md ├── exercise-5.md ├── exercise-answer-key.md └── images │ ├── exercise-1 │ ├── filter-snapshot.gif │ ├── module-select.png │ ├── open-profiler.png │ ├── record-allocation-timeline.gif │ ├── take-heap-snapshot.gif │ ├── timeline-columns.png │ ├── tracking-down-a-leak.gif │ └── zoomed-allocation.png │ ├── exercise-2 │ ├── finding-the-leak-code.gif │ └── identifying-the-leak.gif │ └── exercise-3 │ └── finding-the-scope-leak.gif ├── package.json ├── public ├── crossdomain.xml └── robots.txt ├── testem.js ├── tests ├── .eslintrc.js ├── acceptance │ └── main-test.js ├── helpers │ ├── destroy-app.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ └── list-view-test.js ├── test-helper.js └── unit │ ├── .gitkeep │ ├── controllers │ └── application-test.js │ ├── helpers │ └── format-name-test.js │ ├── routes │ └── application-test.js │ └── services │ └── shared-storage-test.js ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 6, 5 | sourceType: 'module' 6 | }, 7 | extends: 'eslint:recommended', 8 | env: { 9 | browser: true 10 | }, 11 | rules: { 12 | 'semi': 2 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | testem.log 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "7" 5 | 6 | sudo: false 7 | 8 | cache: 9 | yarn: true 10 | directories: 11 | - $HOME/.npm 12 | - $HOME/.cache # includes bowers cache 13 | 14 | before_install: 15 | - npm config set spin false 16 | - npm install -g bower phantomjs-prebuilt 17 | - bower --version 18 | - phantomjs --version 19 | 20 | install: 21 | - npm install 22 | - bower install 23 | 24 | script: 25 | - npm test 26 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memory Leak Examples [](https://travis-ci.org/ember-best-practices/memory-leak-examples) 2 | 3 | This is a simple training application to help you get comfortable finding memory leaks and fixing them in the context of an actual web application. While this application is an [Ember](http://emberjs.com/) app, there is little in this training that is specific to Ember and pre-requisite knowledge of Ember is not required in order to do any of the exercises provided. 4 | 5 | ## Prerequisites 6 | 7 | You will need the following things properly installed on your computer in order to get the application up and running and to work through the exercises: 8 | 9 | * [Git](https://git-scm.com/) 10 | * [Node.js](https://nodejs.org/) 11 | * [Yarn](https://yarnpkg.com/) 12 | * [Ember CLI](https://ember-cli.com/) 13 | * [Chrome Canary](https://www.google.com/chrome/browser/canary.html) (_optional_) 14 | 15 | ## Installation 16 | 17 | Once you have the prerequisites setup on your machine, you can install this applications and its dependencies with the following commands: 18 | 19 | * `git clone https://github.com/ember-best-practices/memory-leak-examples.git` 20 | * `cd memory-leak-examples` 21 | * `yarn install`* 22 | 23 | *_Note: Yarn is used to ensure that dependencies (including nested dependencies) are locked to a specific version. You can use `npm install`, but the results of the exercises may vary._ 24 | 25 | ## Exercises 26 | 27 | After completing installation, jump into the [`exercises` directory](./exercises/exercise-0.md). From there, just read through the exercises in order and you should do just fine. Good luck! 28 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | import './utils/component-debug'; 7 | 8 | let App; 9 | 10 | Ember.MODEL_FACTORY_INJECTIONS = true; 11 | 12 | App = Ember.Application.extend({ 13 | modulePrefix: config.modulePrefix, 14 | podModulePrefix: config.podModulePrefix, 15 | Resolver 16 | }); 17 | 18 | loadInitializers(App, config.modulePrefix); 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/current-route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | computed: { 5 | readOnly 6 | }, 7 | inject: { 8 | service 9 | } 10 | } = Ember; 11 | 12 | export default Ember.Component.extend({ 13 | storage: service('shared-storage'), 14 | 15 | init() { 16 | this._super(...arguments); 17 | this.set('appController', this.get('storage').get('application-controller')); 18 | }, 19 | 20 | currentRoute: readOnly('appController.currentPath') 21 | }); 22 | -------------------------------------------------------------------------------- /app/components/list-view.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | didInsertElement() { 5 | if (this.get('onScroll')) { 6 | window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | inject: { 5 | service 6 | } 7 | } = Ember; 8 | 9 | export default Ember.Controller.extend({ 10 | storage: service('shared-storage'), 11 | 12 | init() { 13 | this._super(...arguments); 14 | 15 | // Save the application controller for use with other things 16 | this.get('storage').set('application-controller', this); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | actions: { 5 | onScroll() { 6 | if (!this.isDestroyed) { 7 | this.set('scrollDistance', window.scrollY); 8 | this.set('scrollMax', document.body.scrollHeight - window.innerHeight); 9 | } 10 | } 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/helpers/capitalize.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | String: { 5 | capitalize 6 | }, 7 | Helper: { 8 | helper 9 | } 10 | } = Ember; 11 | 12 | export function capitalizeHelper([str]) { 13 | return capitalize(str); 14 | } 15 | 16 | export default helper(capitalizeHelper); 17 | -------------------------------------------------------------------------------- /app/helpers/format-name.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { 4 | get, 5 | Map, 6 | Helper: { 7 | helper 8 | } 9 | } = Ember; 10 | const cache = new Map(); 11 | 12 | export function formatName([user]) { 13 | if (cache.get(user)) { 14 | return cache.get(user); 15 | } 16 | 17 | let name = `${get(user, 'firstName')} ${get(user, 'lastName')}`; 18 | 19 | cache.set(user, name); 20 | 21 | return name; 22 | } 23 | 24 | export default helper(formatName); 25 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |You are here: {{capitalize currentRoute}}
2 | -------------------------------------------------------------------------------- /app/templates/components/list-view.hbs: -------------------------------------------------------------------------------- 1 | {{#each items as |item|}} 2 |6 | {{#link-to 'profile' update.user.id}}{{format-name update.user}}{{/link-to}} 7 |
8 |{{update.body}}
11 | {{/list-view}} 12 | -------------------------------------------------------------------------------- /app/templates/profile.hbs: -------------------------------------------------------------------------------- 1 |{{update.body}}
5 | {{/list-view}} 6 | -------------------------------------------------------------------------------- /app/utils/component-debug.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | Ember.Component.reopen({ 5 | init() { 6 | let component = this; 7 | component._super(...arguments); 8 | 9 | if (config.environment !== 'production') { 10 | let id = component.toString(); 11 | window.lastComponentRendered = function() { return id; }; 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memory-leak-examples", 3 | "dependencies": { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'memory-leak-examples', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | } 44 | 45 | if (environment === 'production') { 46 | 47 | } 48 | 49 | return ENV; 50 | }; 51 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberApp(defaults, { 7 | trees: { 8 | vendor: 'node_modules/bulma' 9 | } 10 | }); 11 | 12 | // Use `app.import` to add additional libraries to the generated 13 | // output files. 14 | // 15 | // If you need to use different assets in different 16 | // environments, specify an object as the first parameter. That 17 | // object's keys should be the environment name and the values 18 | // should be the asset to use in that environment. 19 | // 20 | // If the library that you are including contains AMD or ES6 21 | // modules that you would like to import into your application 22 | // please specify an object with the list of modules as keys 23 | // along with the exports of each module as its value. 24 | 25 | app.import('vendor/css/bulma.css'); 26 | 27 | return app.toTree(); 28 | }; 29 | -------------------------------------------------------------------------------- /exercises/exercise-0.md: -------------------------------------------------------------------------------- 1 | # Exercise #0 - Introduction 2 | 3 | Welcome to the training! 4 | 5 | If you've made it this far, then you should have all the prerequisite tools and 6 | dependencies installed. If you haven't done that yet, you should jump back to 7 | the [`README`](../README.md). 8 | 9 | ## Starting Your Environment 10 | 11 | The first thing we need to do is orient ourselves with our development 12 | environment. 13 | 14 | Make sure your current working directory is the root of this repo and run the 15 | following command: 16 | 17 | ```bash 18 | ember serve 19 | ``` 20 | 21 | Now, you should be able to visit the application in your web browser at 22 | [http://localhost:4200/](http://localhost:4200/). 23 | 24 | Click around the app a bit and get a feel for it. The application is relatively 25 | simplistic and is read-only in terms of data consumption. 26 | 27 | Once you've oriented yourself, visit [http://localhost:4200/tests/](http://localhost:4200/tests/) 28 | and check out the tests for the application, which should all be passing. This 29 | is where we will be spending the majority of our time since the automated 30 | execution of tests makes identifying and fixing memory leaks much easier than 31 | within the application itself. 32 | 33 | One last thing to do before we move on: try modifying `app/app.js`. I recommend 34 | doing something like adding `console.log('boo!')` and then saving the file. This 35 | should cause the browser to auto-reload with your changes. By default, Ember is 36 | watching the files in our `app` and `tests` directories, so we should get quick 37 | feedback when working through these exercises. 38 | 39 | ## Using Chrome Canary 40 | 41 | Some developers recommend using Chrome Canary whenever you do profiling and 42 | debugging work for applications. The reason for this is that Chrome Canary often 43 | has the latest and greatest in terms of developer tools. It also gives you access 44 | to running special feature flags should you need them. 45 | 46 | For the purpose of these exercises, you can use Chrome Canary if you 47 | want to check it out, but using the current stable version of Chrome is also 48 | fine (v57 as of this writing). All screenshots and instructions will be from the 49 | current stable version of Chrome. 50 | 51 | [Next: Exercise #1](./exercise-1.md) 52 | -------------------------------------------------------------------------------- /exercises/exercise-1.md: -------------------------------------------------------------------------------- 1 | # Exercise #1 - Prototype Reference Leaks 2 | 3 | This first exercise will look at "prototype reference" leaks. These are memory 4 | leaks that occur because state is stored via a reference on a class' prototype. 5 | 6 | Let's dive in and see how to find and fix them! 7 | 8 | ## Identifying Memory Leaks 9 | 10 | We're going to get right into "doing" and save the more conceptual discussion of 11 | "how to identify memory leaks" for later. 12 | 13 | Let's visit our [test page](http://localhost:4200/tests/) as covered in the 14 | introduction and select the "_Unit | Service | shared storage_" module from the 15 | dropdown. 16 | 17 |  18 | 19 | This module only has one [smoke test](https://en.wikipedia.org/wiki/Smoke_testing_(software)) 20 | currently, but it is passing so we can be assured the basic functionality is 21 | working as expected. However, we have a hunch that it may be causing a memory 22 | leak, but we want to dive in and verify our assumptions before trying to change 23 | any code. 24 | 25 | > You should **always verify** behavior before changing it. If you suspect a 26 | > memory leak, verify it before changing existing code, especially if the 27 | > changes will increase complexity. 28 | 29 | The first thing we want to do is open up our memory profiler. We do this by 30 | opening the Chrome Developer Tools (`Ctrl + Shift + I` on Windows, 31 | `Cmd + Opt + I` on Mac) and selecting the "Profiles" tab. 32 | 33 |  34 | 35 | This tab has some powerful features in it and you should try to familiarize 36 | yourself with all of them at some point, but today we are really only concerned 37 | with two of the options: "Record Allocation Timeline" and "Take Heap Snapshot". 38 | 39 | ### Record Allocation Timeline 40 | 41 | Since we suspect a leak in our code, we want to take a look at our application's 42 | memory allocation over time. In a healthy app with no memory leaks, we would 43 | expect little, if any, memory to be retained after a test in our suite finishes. 44 | So, we can use the "Record Allocation Timeline" feature to look at how we are 45 | using memory over the course of our test run. 46 | 47 | It's pretty simple. From the test page, click "Start" to begin recording, 48 | reload the page, and then hit the red recording button to stop when the tasks 49 | you are concerned with finish; in our case that would be after the test finishes. 50 | 51 |  52 | 53 | Now that we have a recording, we can take a look at it. 54 | 55 | You'll notice that the timeline is primarily a series of columns with some blue 56 | and gray portions to them. Each column represents memory allocated during that 57 | moment in time; the gray part is memory that was then subsequently released and 58 | the blue is memory that is still being retained. 59 | 60 |  61 | 62 | For applications with memory leaks, you will see many blue columns when 63 | recording over long periods of time. The cumulative effect of all this retained 64 | memory would be a leak that impacts the performance of your application. 65 | 66 | A quick word of warning here, _not all retained memory is due to leaks_. 67 | Sometimes retained memory is simply made up of new function definitions, 68 | strings, or maybe the evaluation of a JS module, while these add up over time, 69 | they should be a one time cost and not considered leaks. 70 | 71 | We can look at either the whole recorded timeline, or a portion, by dragging the 72 | window in the summary view. 73 | 74 |  75 | 76 | Looking at the timeline recording, it seems like we may have a leak because 77 | there is some blue that is not getting cleaned up around the time the test 78 | executes. However, we're still a little unsure, so let's use another tool to 79 | help us verify. 80 | 81 | ### Take Heap Snapshot 82 | 83 | For Ember applications we are primarily worried with leaking one specific 84 | construct: the `container`. This is because the `container` holds all the state 85 | for our application (or test) and thus, if it is not cleaned up, we're going 86 | to use up memory very quickly. 87 | 88 | Thankfully, since we know what might be leaking, we can check for it easily. We 89 | can take a "heap snapshot" at any point in time to see what memory is 90 | currently being used. Simply choose "Take Heap Snapshot" as the profiling type 91 | and then click the "record" button. 92 | 93 |  94 | 95 | If we do this after running our test, then we will be able to see all the 96 | objects currently held in memory, including those introduced (or "leaked") by 97 | our test. However, this is a lot of information, and most of it isn't 98 | particularly interesting. 99 | 100 | Since we know what we're looking for, we can quickly filter through the retained 101 | objects by using the "class filter" at the top of the summary view. In this 102 | case, we're looking for objects of the `Container` class. 103 | 104 |  105 | 106 | We can see that we have one retained. This is bad, because it should have been 107 | cleaned up before. This could lead to a potentially large memory leak when 108 | running more than just one test. 109 | 110 | So, how do we fix it? Well, we need to figure out why this is being "retained" 111 | in memory. 112 | 113 | If we click on the object in the snapshot, we'll get a list of "retainers" at 114 | the bottom of the profiling tab. This will show you the references to the 115 | selected object that are keeping it in memory. The "distance" will let you know 116 | how many references it takes to get to the global context. 117 | 118 | From here, the process is a bit fuzzy. Essentially, we follow the "shortest" 119 | path to the global and look for objects that are part of our application's code. 120 | In this case, as we walk up the retainer tree, we find the `_data` property 121 | which is part of our service's class. 122 | 123 |  124 | 125 | We should expect the `_data` property to get cleaned up when the service is 126 | destroyed (e.g., at the end of the test). So, it is likely that this is the 127 | source of our memory leak. 128 | 129 | If we take a look at the source code: 130 | 131 | ```js 132 | // app/services/shared-storage.js 133 | export default Ember.Service.extend({ 134 | _data: Object.create(null) 135 | }); 136 | ``` 137 | 138 | We'll notice that the `_data` property is set to an object during `extend`. This 139 | means that it is being placed on the `prototype` of the class. Since `protoypes` 140 | are never destroyed, any changes to the `_data` property will stick around 141 | indefinitely. 142 | 143 | > It is worth noting that this is primarily a problem since Object's are 144 | > non-primitive values, meaning that as we modify them, the value isn't replaced. 145 | > For primitive values, if we modify them on an instance of a class, it will no 146 | > longer be modifying the `prototype`, but will instead modify the instance 147 | > directly. 148 | 149 | So, how do we fix this? Thankfully, it is easy, all we need to do is set `_data` 150 | on each _instance_ of the class, rather than on the class itself. We can do this 151 | during `init`, which can be thought of as the constructor function: 152 | 153 | ```js 154 | export default Ember.Service.extend({ 155 | init() { 156 | this._super(...arguments); 157 | this._data = Object.create(null); 158 | } 159 | }); 160 | ``` 161 | 162 | And that's it! You should now be able to re-run the test, take a heap snapshot, 163 | and verify that the memory leak is no longer present. 164 | 165 | ## Key Takeaways 166 | 167 | * Use "Record Allocation Timeline" to identify the presence of memory leaks. 168 | * Use "Take Heap Snapshot" to verify leaked objects and figure out why they're 169 | leaking. 170 | * Do _NOT_ set non-primitive values on a class' `prototype`. Instead, initialize 171 | them during the class' instantiation. 172 | 173 | [Prev: Exercise #0](./exercise-0.md) | [Next: Exercise #2](./exercise-2.md) 174 | -------------------------------------------------------------------------------- /exercises/exercise-2.md: -------------------------------------------------------------------------------- 1 | # Exercise #2 - Callback Leaks 2 | 3 | This exercise will build on what we covered in [Exercise #1](./exercise-1.md) 4 | and will look at "callback" leaks. These are memory leaks that occur due to 5 | state being caught in a callback function that is never released from memory. 6 | 7 | ## Identifying The Leak 8 | 9 | Similar to before, we need to start by identifying the leak. The test we're 10 | concerned with is "_Integration | Component | list view_". 11 | 12 | Practice using "Record Allocation Timeline" and "Take Heap Snapshot" to figure 13 | out what might be the leak before taking a look below. 14 | 15 | --- 16 | 17 |  18 | 19 | In the above gif, you can see that you can actually use the "Record Allocation 20 | Timeline" to find leaked objects. Since we're concerned with objects leaking 21 | during the tests, we simply narrow the window to that time range and then use 22 | the class filter. This is similar to what we previously did with "Take Heap 23 | Snapshot" in Exercise #1. 24 | 25 | We can then look through the retainers like we did previously to try and figure 26 | out where the leak is happening in the actual code. 27 | 28 |  29 | 30 | There are two things to note in the above: 31 | 32 | First, we're interested in `_this`, which was retained by `context in ()`. 33 | When you encounter a pattern like this for the first time, it can look very odd. 34 | The `_this` is a variable introduced during transpilation by Babel. The 35 | `context in ()` is telling us that `_this` is retained in context by an 36 | anonymous function. 37 | 38 | In other words, our original code: 39 | 40 | ```js 41 | export default Ember.Component.extend({ 42 | didInsertElement() { 43 | if (this.get('onScroll')) { 44 | window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); 45 | } 46 | } 47 | }); 48 | ``` 49 | 50 | Is transpiled to something like: 51 | 52 | ```js 53 | export default Ember.Component.extend({ 54 | didInsertElement() { 55 | if (this.get('onScroll')) { 56 | var _this = this; 57 | window.addEventListener('scroll', function(...args) { return _this.get('onScroll')(...args) }); 58 | } 59 | } 60 | }); 61 | ``` 62 | 63 | Which should make it obvious how `_this` is retained by the context of an 64 | anonymous function. This should not be happening, we don't want this to stay 65 | around after the life of the component. 66 | 67 | Second, note that you can hover to link to code. Once we have identified an area 68 | of interest, we are able to hover over the `context in ()` phrase and link 69 | the exact spot in the code where it occurred. This is very useful for debugging 70 | retainers that are in context/scope. 71 | 72 | ## Fixing the Leak 73 | 74 | This is a callback leak. Since callback functions for things like event 75 | listeners and interval timers are retained by reference elsewhere, you 76 | must be careful to unregister them when no longer needed or ensure that 77 | the context they're registered with is destroyed. In this case, Since 78 | `window` is never destroyed/removed, this callback will continue to 79 | exist forever, and since its callback closes over a reference to this 80 | component, we have a bad memory leak. The solution is to remove this 81 | event listener in `willDestroy`. 82 | 83 | So, we can update our original component implementation to look something like 84 | this: 85 | 86 | ```js 87 | export default Ember.Component.extend({ 88 | didInsertElement() { 89 | if (this.get('onScroll')) { 90 | this._onScrollHandler = (...args) => this.get('onScroll')(...args); 91 | window.addEventListener('scroll', this._onScrollHandler); 92 | } 93 | }, 94 | 95 | willDestroy() { 96 | window.removeEventListener('scroll', this._onScrollHandler); 97 | } 98 | }); 99 | ``` 100 | 101 | And that should fix our memory leak! 102 | 103 | At this point you should run the tests again and check to make sure the leak 104 | was actually fixed. You should _always verify your work_, particularly when it 105 | comes to nuanced tasks like fixing memory leaks or improving performance. 106 | 107 | If you did everything correctly, you should discover that the memory leak isn't 108 | fixed yet! But, if you look closely, you'll notice that it isn't the same one as 109 | before. To dig into that, we'll move to the next exercise. 110 | 111 | ## Key Takeaways 112 | 113 | * Be careful when registering callbacks; ensure they get cleaned up. 114 | * Sometimes memory leaks just by being in context/scope of a retained function. 115 | * There may be more than one thing leaking a given object. 116 | 117 | [Prev: Exercise #1](./exercise-1.md) | [Next: Exercise #3](./exercise-3.md) 118 | -------------------------------------------------------------------------------- /exercises/exercise-3.md: -------------------------------------------------------------------------------- 1 | # Exercise #3 - Scope Leaks 2 | 3 | This exercise will pick up right where [Exercise #2](./exercise-2.md) left off 4 | and will look at "scope" leaks. These are memory leaks that occur due to state 5 | being accessible via the scope of some retained object in memory. 6 | 7 | ## Identifying The Leak 8 | 9 | Exercise #2 left off with the "_Integration | Component | list view_" test still 10 | having a memory leak after fixing one. At this point, you should be relatively 11 | comfortable with using the memory profiler to identify where a memory leak is 12 | coming from, so we will gloss over some steps. 13 | 14 | ## Fixing The Leak 15 | 16 |  17 | 18 | Here, we have another `context in ()` in our retainer graph, but this time it 19 | does not look like we're actually referencing any variables in the function 20 | which seems to be leaking: 21 | 22 | ```js 23 | init() { 24 | let component = this; 25 | component._super(...arguments); 26 | 27 | if (config.environment !== 'production') { 28 | let id = component.toString(); 29 | window.lastComponentRendered = function() { return id; }; 30 | } 31 | } 32 | ``` 33 | 34 | In theory, it seems like the `lastComponentRendered` function above should not 35 | be causing the `component` to leak. However, since `component` is still within 36 | the accessible scope of the function, it is retained in memory. Thus, we call 37 | this a scope leak because we are leaking state indirectly via the scope a 38 | function has access to. 39 | 40 | So, how do we fix this? Well, simply remove the variable from scope _or_ move 41 | the function to a different scope. Here we do both: 42 | 43 | ```js 44 | Ember.Component.reopen({ 45 | init() { 46 | this._super(...arguments); 47 | this._setupLastComponentRendered(); 48 | }, 49 | 50 | _setupLastComponentRendered() { 51 | if (config.environment !== 'production') { 52 | let id = this.toString(); 53 | window.lastComponentRendered = function() { return id; }; 54 | } 55 | } 56 | }); 57 | ``` 58 | 59 | But, if we check the profiler again, this doesn't seem to fix our leak. Why? 60 | 61 | Well the reason is that Babel transpiles `let` statements that are within a 62 | different block into something like this: 63 | 64 | ```js 65 | _setupLastComponentRender() { 66 | var _this = this; // GRR! This is annoying... 67 | 68 | if (config.environment !== 'production') { 69 | (function () { // So is this! 70 | var id = _this.toString(); 71 | window.lastComponentRendered = function () { 72 | return id; 73 | }; 74 | })(); 75 | } 76 | } 77 | ``` 78 | 79 | And the introduced `_this` variable is now leaking into our scope again. 80 | 81 | We can fix this by making the scope of the `let` and the function the same, like 82 | so: 83 | 84 | ```js 85 | Ember.Component.reopen({ 86 | init() { 87 | this._super(...arguments); 88 | 89 | if (config.environment !== 'production') { 90 | this._setupLastComponentRendered(); 91 | } 92 | }, 93 | 94 | _setupLastComponentRendered() { 95 | let id = this.toString(); 96 | window.lastComponentRendered = function() { return id; }; 97 | } 98 | }); 99 | ``` 100 | 101 | This has the added benefit of making it easier to tell which steps during `init` 102 | are actually executed in a given environment as well. Overall, this pattern is 103 | likely a better approach, and it actually fixes our issue! 104 | 105 | So, with that you should have successfully fixed all the leaks in this test. 106 | 107 | ## Key Takeaways 108 | 109 | * Don't assume a fix actually fixed a leak until verification occurs. 110 | * Be wary of transpilation side-effects. While Babel and others do well in most 111 | circumstances, they can indirectly cause issues. 112 | * Scope leaks occur just by having access to variables in scope, even if 113 | they're not used. 114 | * To fix scope leaks, extract closure creation into functions with just the 115 | right amount of state in scope. 116 | 117 | [Prev: Exercise #2](./exercise-2.md) | [Next: Exercise #4](./exercise-4.md) 118 | -------------------------------------------------------------------------------- /exercises/exercise-4.md: -------------------------------------------------------------------------------- 1 | # Exercise #4 - Module Leaks 2 | 3 | Now that you have successfully identified and fixed several memory leaks, we're 4 | going to change things up a bit and talk about a certain kind of memory leak 5 | without a specific test to follow. 6 | 7 | ## The Pattern 8 | 9 | Module memory leaks occur when state is kept within a [JS module](http://2ality.com/2014/09/es6-modules-final.html) 10 | and has no explicit mechanism to remove or reset it at any point in time. 11 | 12 | An easy example would be like so: 13 | 14 | ```js 15 | const cache = new Map(); 16 | export default function complexTask(userObj) { 17 | if (cache.get(userObj)) { 18 | return cache.get(userObj); 19 | } 20 | 21 | // ... 22 | 23 | cache.set(userObj, result); 24 | 25 | return result; 26 | } 27 | ``` 28 | 29 | In the above, any value that is ever stored in `cache` will be retained forever. 30 | This is because once a JS module is evaluated, the code remains in memory 31 | indefinitely. This is similar to the "scope" leak in that it is a by-product of 32 | the exported function having closed over the scope in which `cache` is defined. 33 | 34 | So, how does one fix a leak like this? Well, we have several options. 35 | 36 | First, we should evaluate if `cache` is even needed. There is the chance that 37 | if arguments to a given function are non-primitive values, then caching may 38 | actually lead to more bugs. 39 | 40 | Second, cache primitives instead of references, we should try, as much as 41 | possible not to store references to input values in our cache. Doing so, makes 42 | it very easy to leak objects which you never intended to be stored elsewhere. 43 | 44 | Finally, if a cache is needed and it must store non-primitive information, a 45 | caching service that knows to reset itself on destruction of the host 46 | application is a good approach. We essentially want a mechanism to ensure the 47 | cache does not outlive the lifespan of its host. The exact approach here will 48 | vary depending on your framework. 49 | 50 | In the above example, we're currently using an input object as our cache key, 51 | but if we only need two properties, maybe `foo` and `bar`, we could use those 52 | as our key instead: 53 | 54 | ```js 55 | let key = userObj.foo + userObj.bar; 56 | if (cache.get(key)) { 57 | // ... 58 | ``` 59 | 60 | ## Key Takeaways 61 | 62 | * Be wary of placing state in module scope. 63 | * Favor primitive values over references when possible. 64 | 65 | [Prev: Exercise #3](./exercise-3.md) | [Next: Exercise #5](./exercise-5.md) 66 | -------------------------------------------------------------------------------- /exercises/exercise-5.md: -------------------------------------------------------------------------------- 1 | # Exercise #5 - Fixing Leaks On Your Own 2 | 3 | At this point, we have talked about the most common types of memory leaks and 4 | learned how to identify and fix them for Ember applications. You should feel 5 | relatively comfortable digging in and finding leaks for systems where you know 6 | what might be the culprit. 7 | 8 | For the last exercise in this training, you are expected to run the entire test 9 | suite for the app and fix any remaining leaks of the `container` object. 10 | 11 | There is an answer key on the next page, but we encourage you to try and fix 12 | everything on your own before taking a look. 13 | 14 | ## Key Takeaways 15 | 16 | * You can successfully fix memory leaks in an Ember application without help! 17 | 18 | [Prev: Exercise #4](./exercise-4.md) | [Next: Answer Key](./exercise-answer-key.md) 19 | -------------------------------------------------------------------------------- /exercises/exercise-answer-key.md: -------------------------------------------------------------------------------- 1 | # Answer Key 2 | 3 | The following provides answers for each exercise in the training. It will specify which file the leak occurs in as well as a sample code change you can make to fix the leak. 4 | 5 | ## 1. app/services/shared-storage.js 6 | 7 | ```js 8 | export default Ember.Service.extend({ 9 | _data: Object.create(null) 10 | }); 11 | ``` 12 | 13 | ```js 14 | export default Ember.Service.extend({ 15 | init() { 16 | this._data = Object.create(null); 17 | } 18 | }); 19 | ``` 20 | 21 | ## 2. app/components/list-view.js 22 | 23 | ```js 24 | export default Ember.Component.extend({ 25 | didInsertElement() { 26 | if (this.get('onScroll')) { 27 | window.addEventListener('scroll', (...args) => this.get('onScroll')(...args)); 28 | } 29 | } 30 | }); 31 | ``` 32 | 33 | ```js 34 | export default Ember.Component.extend({ 35 | _onScrollHandler: null, 36 | 37 | didInsertElement() { 38 | if (this.get('onScroll')) { 39 | this._onScrollHandler = this.get('onScroll').bind(this); 40 | window.addEventListener('scroll', this._onScrollHandler); 41 | } 42 | }, 43 | 44 | willDestroy() { 45 | if (this._onScrollHandler) { 46 | window.removeEventListener('scroll', this._onScrollHandler); 47 | } 48 | } 49 | }); 50 | ``` 51 | 52 | ## 3. app/utils/component-debug.js 53 | 54 | ```js 55 | Ember.Component.reopen({ 56 | init() { 57 | let component = this; 58 | component._super(...arguments); 59 | 60 | if (config.environment !== 'production') { 61 | let id = component.toString(); 62 | window.lastComponentRendered = function() { return id; }; 63 | } 64 | } 65 | }); 66 | ``` 67 | 68 | ```js 69 | Ember.Component.reopen({ 70 | init() { 71 | this._super(...arguments); 72 | 73 | if (config.environment !== 'production') { 74 | this._setupLastComponentRendered(); 75 | } 76 | }, 77 | 78 | _setupLastComponentRendered() { 79 | let id = this.toString(); 80 | window.lastComponentRendered = function() { return id; }; 81 | } 82 | }); 83 | ``` 84 | 85 | ## 5a. app/instance-initializers/link-tracking.js 86 | 87 | ```js 88 | export function initialize(instance) { 89 | LinkComponent.reopen({ 90 | click() { 91 | this._super(...arguments); 92 | trackLink(this); 93 | } 94 | }); 95 | 96 | function trackLink(link) { 97 | let dest = link.get('targetRouteName'); 98 | let src = instance.lookup('router:main').currentRouteName; 99 | 100 | // eslint-disable-next-line no-console 101 | console.log(`Taking link from ${src} to ${dest}`); 102 | } 103 | } 104 | ``` 105 | 106 | ```js 107 | export function initialize() { 108 | LinkComponent.reopen({ 109 | click() { 110 | this._super(...arguments); 111 | this.trackLink(); 112 | }, 113 | 114 | trackLink() { 115 | let dest = this.get('targetRouteName'); 116 | let src = Ember.getOwner(this).lookup('router:main').currentRouteName; 117 | 118 | // eslint-disable-next-line no-console 119 | console.log(`Taking link from ${src} to ${dest}`); 120 | 121 | } 122 | }); 123 | } 124 | ``` 125 | 126 | ## 5b. app/helpers/format-name.js 127 | 128 | ```js 129 | const cache = new Map(); 130 | 131 | export function formatName([user]) { 132 | if (cache.get(user)) { 133 | return cache.get(user); 134 | } 135 | 136 | let name = `${get(user, 'firstName')} ${get(user, 'lastName')}`; 137 | 138 | cache.set(user, name); 139 | 140 | return name; 141 | } 142 | ``` 143 | 144 | ```js 145 | export function formatName([user]) { 146 | return `${get(user, 'firstName')} ${get(user, 'lastName')}`; 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /exercises/images/exercise-1/filter-snapshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/filter-snapshot.gif -------------------------------------------------------------------------------- /exercises/images/exercise-1/module-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/module-select.png -------------------------------------------------------------------------------- /exercises/images/exercise-1/open-profiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/open-profiler.png -------------------------------------------------------------------------------- /exercises/images/exercise-1/record-allocation-timeline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/record-allocation-timeline.gif -------------------------------------------------------------------------------- /exercises/images/exercise-1/take-heap-snapshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/take-heap-snapshot.gif -------------------------------------------------------------------------------- /exercises/images/exercise-1/timeline-columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/timeline-columns.png -------------------------------------------------------------------------------- /exercises/images/exercise-1/tracking-down-a-leak.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/tracking-down-a-leak.gif -------------------------------------------------------------------------------- /exercises/images/exercise-1/zoomed-allocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-1/zoomed-allocation.png -------------------------------------------------------------------------------- /exercises/images/exercise-2/finding-the-leak-code.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-2/finding-the-leak-code.gif -------------------------------------------------------------------------------- /exercises/images/exercise-2/identifying-the-leak.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-2/identifying-the-leak.gif -------------------------------------------------------------------------------- /exercises/images/exercise-3/finding-the-scope-leak.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/exercises/images/exercise-3/finding-the-scope-leak.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "memory-leak-examples", 3 | "version": "0.0.0", 4 | "description": "Small description for memory-leak-examples goes here", 5 | "license": "MIT", 6 | "author": "", 7 | "directories": { 8 | "doc": "doc", 9 | "test": "tests" 10 | }, 11 | "repository": "", 12 | "scripts": { 13 | "build": "ember build", 14 | "start": "ember server", 15 | "test": "ember test" 16 | }, 17 | "devDependencies": { 18 | "broccoli-asset-rev": "^2.4.5", 19 | "ember-ajax": "^2.4.1", 20 | "ember-cli": "2.11.1", 21 | "ember-cli-app-version": "^2.0.0", 22 | "ember-cli-babel": "^5.1.7", 23 | "ember-cli-dependency-checker": "^1.3.0", 24 | "ember-cli-eslint": "^3.0.2", 25 | "ember-cli-htmlbars": "^1.1.1", 26 | "ember-cli-htmlbars-inline-precompile": "^0.3.6", 27 | "ember-cli-inject-live-reload": "^1.4.1", 28 | "ember-cli-qunit": "^3.0.1", 29 | "ember-cli-release": "^0.2.9", 30 | "ember-cli-shims": "^1.0.2", 31 | "ember-cli-sri": "^2.1.0", 32 | "ember-cli-test-loader": "^1.1.0", 33 | "ember-cli-uglify": "^1.2.0", 34 | "ember-data": "^2.11.0", 35 | "ember-export-application-global": "^1.0.5", 36 | "ember-load-initializers": "^0.6.0", 37 | "ember-resolver": "^2.0.3", 38 | "ember-source": "~2.11.0", 39 | "ember-welcome-page": "^2.0.2", 40 | "loader.js": "^4.0.10" 41 | }, 42 | "engines": { 43 | "node": ">= 0.12.0" 44 | }, 45 | "private": true, 46 | "dependencies": { 47 | "bulma": "^0.3.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |{{item.name}}
13 | {{/list-view}} 14 | `); 15 | 16 | assert.equal(this.$('.name').text().trim(), 'foobar'); 17 | }); 18 | 19 | test('it sets up a scroll handler', function(assert) { 20 | function scrollHandler() { 21 | assert.step('scroll'); 22 | } 23 | 24 | this.set('items', [{ name: 'foo' }, { name: 'bar' }]); 25 | this.set('scrollHandler', scrollHandler); 26 | 27 | this.render(hbs` 28 | {{#list-view onScroll=scrollHandler items=items as |item|}} 29 |{{item.name}}
30 | {{/list-view}} 31 | `); 32 | 33 | window.dispatchEvent(new CustomEvent('scroll')); 34 | 35 | assert.verifySteps(['scroll']); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/controllers/application-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:application', 'Unit | Controller | application', { 4 | needs: [ 5 | 'service:shared-storage' 6 | ] 7 | }); 8 | 9 | test('it exists', function(assert) { 10 | let controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/helpers/format-name-test.js: -------------------------------------------------------------------------------- 1 | 2 | import { formatName } from 'memory-leak-examples/helpers/format-name'; 3 | import { module, test } from 'qunit'; 4 | 5 | module('Unit | Helper | format name'); 6 | 7 | test('it works', function(assert) { 8 | let result = formatName([42]); 9 | assert.ok(result); 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /tests/unit/routes/application-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:application', 'Unit | Route | application'); 4 | 5 | test('it exists', function(assert) { 6 | let route = this.subject(); 7 | assert.ok(route); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/unit/services/shared-storage-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:shared-storage', 'Unit | Service | shared storage'); 4 | 5 | test('smoke test', function(assert) { 6 | let service = this.subject(); 7 | 8 | service.set('subject', service); 9 | service.set('falsy', ''); 10 | 11 | assert.strictEqual(service.get('subject'), service, 'set/get - works for truthy values'); 12 | assert.strictEqual(service.get('falsy'), '', 'set/get - works for falsy values'); 13 | 14 | assert.strictEqual(service.has('subject'), true, 'has - works for truthy values'); 15 | assert.strictEqual(service.has('falsy'), true, 'has - works for false values'); 16 | 17 | service.remove('subject'); 18 | service.remove('falsy'); 19 | 20 | assert.strictEqual(service.has('subject'), false, 'remove - works for truthy values'); 21 | assert.strictEqual(service.has('falsy'), false, 'remove - works for false values'); 22 | 23 | service.set('subject', service); 24 | service.set('falsy', ''); 25 | 26 | assert.strictEqual(service.get('subject'), service, 'set/get - works for reinstated truthy values'); 27 | assert.strictEqual(service.get('falsy'), '', 'set/get - works for reinstated falsy values'); 28 | }); 29 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ember-best-practices/memory-leak-examples/a02c198029f10b81c1cc8e3e992dd065778337ab/vendor/.gitkeep --------------------------------------------------------------------------------