From 5ca60fcb6b592524086f98279d3e4fd949598343 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Tue, 29 Oct 2019 14:52:36 -0400
Subject: [PATCH] REFACTOR: Use imports for `Ember.run`

---
 .../components/admin-backups-logs.js.es6      |  3 +-
 .../components/admin-report-chart.js.es6      |  8 +++--
 .../admin-report-stacked-chart.js.es6         |  8 +++--
 .../admin/components/admin-report.js.es6      |  3 +-
 .../components/admin-theme-editor.js.es6      |  3 +-
 .../components/admin-user-field-item.js.es6   |  3 +-
 .../admin/components/color-input.js.es6       |  5 +--
 .../admin/components/embeddable-host.js.es6   |  3 +-
 .../admin/components/ip-lookup.js.es6         |  3 +-
 .../components/penalty-post-action.js.es6     |  3 +-
 .../admin/components/permalink-form.js.es6    |  5 +--
 .../admin/components/resumable-upload.js.es6  | 14 ++++----
 .../screened-ip-address-form.js.es6           |  5 +--
 .../admin/components/themes-list-item.js.es6  |  3 +-
 .../admin/components/watched-word-form.js.es6 |  5 +--
 .../admin-customize-colors-show.js.es6        |  3 +-
 .../admin-dashboard-reports.js.es6            |  3 +-
 .../admin-logs-staff-action-logs.js.es6       |  3 +-
 .../controllers/admin-site-text-index.js.es6  |  5 +--
 .../controllers/admin-user-badges.js.es6      |  3 +-
 .../admin-watched-words-action.js.es6         |  3 +-
 .../lib/buffered-render.js.es6                |  3 +-
 .../mixins/focus-event.js.es6                 |  3 +-
 app/assets/javascripts/discourse-loader.js    | 12 +++++++
 .../add-category-tag-classes.js.es6           |  3 +-
 .../components/choose-message.js.es6          |  3 +-
 .../discourse/components/choose-topic.js.es6  |  3 +-
 .../discourse/components/composer-body.js.es6 | 15 ++++++---
 .../components/composer-editor.js.es6         | 33 +++++++++++--------
 .../components/composer-messages.js.es6       |  3 +-
 .../components/composer-title.js.es6          | 13 +++++---
 .../components/composer-user-selector.js.es6  |  3 +-
 .../discourse/components/cook-text.js.es6     |  3 +-
 .../discourse/components/d-editor.js.es6      | 23 ++++++++-----
 .../discourse/components/d-modal-body.js.es6  |  3 +-
 .../discourse/components/d-modal.js.es6       |  5 +--
 .../discourse/components/date-input.js.es6    |  3 +-
 .../discourse/components/date-picker.js.es6   |  3 +-
 .../components/discourse-topic.js.es6         | 12 ++++---
 .../components/discovery-topics-list.js.es6   |  8 +++--
 .../components/edit-category-tab.js.es6       |  3 +-
 .../edit-category-topic-template.js.es6       |  3 +-
 .../components/edit-topic-timer-form.js.es6   |  3 +-
 .../components/expanding-text-area.js.es6     |  3 +-
 .../components/flag-selection.js.es6          |  3 +-
 .../discourse/components/footer-nav.js.es6    |  3 +-
 .../discourse/components/global-notice.js.es6 |  5 +--
 .../components/group-flair-inputs.js.es6      |  3 +-
 .../components/image-uploader.js.es6          |  5 +--
 .../discourse/components/link-to-input.js.es6 |  3 +-
 .../discourse/components/login-modal.js.es6   |  3 +-
 .../discourse/components/mobile-nav.js.es6    |  5 +--
 .../discourse/components/mount-widget.js.es6  | 10 +++---
 .../components/navigation-bar.js.es6          |  5 +--
 .../components/pwa-install-banner.js.es6      |  3 +-
 .../discourse/components/quote-button.js.es6  |  3 +-
 .../components/scroll-tracker.js.es6          |  3 +-
 .../components/scrolling-post-stream.js.es6   | 11 ++++---
 .../components/search-advanced-options.js.es6 |  6 ++--
 .../discourse/components/share-panel.js.es6   |  3 +-
 .../discourse/components/share-popup.js.es6   | 10 +++---
 .../discourse/components/signup-cta.js.es6    |  3 +-
 .../discourse/components/site-header.js.es6   |  9 +++--
 .../discourse/components/text-overflow.js.es6 |  3 +-
 .../discourse/components/time-input.js.es6    |  3 +-
 .../components/topic-entrance.js.es6          |  3 +-
 .../discourse/components/topic-list.js.es6    |  3 +-
 .../components/topic-navigation.js.es6        | 10 +++---
 .../components/topic-progress.js.es6          |  9 ++---
 .../components/topic-timeline.js.es6          |  3 +-
 .../components/topic-timer-info.js.es6        |  6 ++--
 .../discourse/components/user-stream.js.es6   |  3 +-
 .../discourse/controllers/auth-token.js.es6   |  3 +-
 .../discourse/controllers/change-owner.js.es6 |  3 +-
 .../controllers/change-timestamp.js.es6       |  3 +-
 .../discourse/controllers/composer.js.es6     |  3 +-
 .../discourse/controllers/exception.js.es6    |  3 +-
 .../controllers/insert-hyperlink.js.es6       |  9 +++--
 .../discourse/controllers/jump-to-post.js.es6 |  3 +-
 .../discourse/controllers/login.js.es6        |  6 ++--
 .../controllers/move-to-topic.js.es6          |  3 +-
 .../second-factor-backup-edit.js.es6          |  3 +-
 .../discourse/controllers/topic.js.es6        |  6 ++--
 .../initializers/asset-version.js.es6         |  3 +-
 .../initializers/auth-complete.js.es6         |  3 +-
 .../discourse/initializers/mobile.js.es6      |  3 +-
 .../javascripts/discourse/lib/ajax.js.es6     |  7 ++--
 .../discourse/lib/autocomplete.js.es6         | 10 +++---
 .../discourse/lib/clean-dom.js.es6            |  3 +-
 .../discourse/lib/click-track.js.es6          |  3 +-
 .../javascripts/discourse/lib/debounce.js.es6 |  3 +-
 .../lib/desktop-notifications.js.es6          |  5 +--
 .../discourse/lib/keyboard-shortcuts.js.es6   |  8 +++--
 .../lib/link-category-hashtags.js.es6         |  3 +-
 .../discourse/lib/link-mentions.js.es6        |  3 +-
 .../discourse/lib/link-tag-hashtag.js.es6     |  3 +-
 .../discourse/lib/load-script.js.es6          |  3 +-
 .../discourse/lib/page-tracker.js.es6         |  3 +-
 .../discourse/lib/safari-hacks.js.es6         |  3 +-
 .../discourse/lib/screen-track.js.es6         |  3 +-
 .../javascripts/discourse/lib/throttle.js.es6 |  3 +-
 .../javascripts/discourse/lib/url.js.es6      | 10 +++---
 .../mixins/card-contents-base.js.es6          |  9 +++--
 .../discourse/mixins/docking.js.es6           |  3 +-
 .../mixins/mobile-scroll-direction.js.es6     |  3 +-
 .../discourse/mixins/scroll-top.js.es6        |  3 +-
 .../discourse/mixins/scrolling.js.es6         |  3 +-
 .../discourse/models/composer.js.es6          |  9 +++--
 .../discourse/routes/application.js.es6       |  6 ++--
 .../discourse/routes/associate-account.js.es6 |  3 +-
 .../discourse/routes/discourse.js.es6         |  3 +-
 .../routes/discovery-categories.js.es6        |  3 +-
 .../discourse/routes/forgot-password.js.es6   |  3 +-
 .../javascripts/discourse/routes/login.js.es6 |  3 +-
 .../discourse/routes/new-message.js.es6       |  5 +--
 .../discourse/routes/new-topic.js.es6         |  3 +-
 .../discourse/routes/signup.js.es6            |  5 +--
 .../discourse/routes/topic-from-params.js.es6 |  3 +-
 .../javascripts/discourse/routes/topic.js.es6 | 13 +++++---
 .../widgets/component-connector.js.es6        |  3 +-
 .../discourse/widgets/connector.js.es6        |  3 +-
 .../javascripts/discourse/widgets/glue.js.es6 |  8 +++--
 .../discourse/widgets/hamburger-menu.js.es6   |  3 +-
 .../discourse/widgets/header.js.es6           |  3 +-
 .../discourse/widgets/post-menu.js.es6        |  6 ++--
 .../discourse/widgets/post-stream.js.es6      |  3 +-
 .../discourse/widgets/search-menu.js.es6      |  6 ++--
 .../discourse/widgets/topic-timeline.js.es6   |  3 +-
 .../discourse/widgets/user-menu.js.es6        |  3 +-
 .../javascripts/pretty-text/oneboxer.js.es6   |  5 +--
 .../pretty-text/upload-short-url.js.es6       |  3 +-
 .../select-kit/mixins/dom-helpers.js.es6      | 14 ++++----
 .../select-kit/mixins/events.js.es6           |  8 +++--
 .../wizard/components/invite-list.js.es6      |  3 +-
 .../wizard/components/radio-button.js.es6     |  3 +-
 .../wizard/components/wizard-step.js.es6      |  5 +--
 app/assets/javascripts/wizard/lib/ajax.js.es6 |  5 +--
 .../javascripts/wizard/lib/preview.js.es6     |  3 +-
 .../wizard/test/acceptance/wizard-test.js.es6 |  3 +-
 .../wizard/test/helpers/start-app.js.es6      |  3 +-
 .../discourse-local-dates-create-form.js.es6  |  7 ++--
 .../composer-presence-display.js.es6          | 13 +++++---
 .../components/topic-presence-display.js.es6  |  6 ++--
 .../acceptance/composer-test.js.es6           |  3 +-
 test/javascripts/acceptance/modal-test.js.es6 |  3 +-
 .../components/d-editor-test.js.es6           |  3 +-
 .../javascripts/controllers/topic-test.js.es6 |  3 +-
 test/javascripts/helpers/qunit-helpers.js.es6 |  6 ++--
 test/javascripts/lib/click-track-test.js.es6  |  3 +-
 test/javascripts/models/nav-item-test.js.es6  |  3 +-
 test/javascripts/widgets/widget-test.js.es6   |  3 +-
 151 files changed, 485 insertions(+), 270 deletions(-)

diff --git a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6
index 67bc308948a..ccacb2e705d 100644
--- a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6
+++ b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import debounce from "discourse/lib/debounce";
 import { renderSpinner } from "discourse/helpers/loading-spinner";
@@ -53,7 +54,7 @@ export default Component.extend(
       // force rerender
       this.rerenderBuffer();
 
-      Ember.run.scheduleOnce("afterRender", this, this._scrollDown);
+      scheduleOnce("afterRender", this, this._scrollDown);
     }, 150),
 
     buildBuffer(buffer) {
diff --git a/app/assets/javascripts/admin/components/admin-report-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-chart.js.es6
index 6ad48208b4a..1f23998d6b8 100644
--- a/app/assets/javascripts/admin/components/admin-report-chart.js.es6
+++ b/app/assets/javascripts/admin/components/admin-report-chart.js.es6
@@ -1,3 +1,5 @@
+import { debounce } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { number } from "discourse/lib/formatter";
 import loadScript from "discourse/lib/load-script";
@@ -11,7 +13,7 @@ export default Component.extend({
     this._super(...arguments);
 
     this.resizeHandler = () =>
-      Ember.run.debounce(this, this._scheduleChartRendering, 500);
+      debounce(this, this._scheduleChartRendering, 500);
   },
 
   didInsertElement() {
@@ -31,11 +33,11 @@ export default Component.extend({
   didReceiveAttrs() {
     this._super(...arguments);
 
-    Ember.run.debounce(this, this._scheduleChartRendering, 100);
+    debounce(this, this._scheduleChartRendering, 100);
   },
 
   _scheduleChartRendering() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this._renderChart(
         this.model,
         this.element && this.element.querySelector(".chart-canvas")
diff --git a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6 b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6
index 1d70da8fed7..77604527ede 100644
--- a/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6
+++ b/app/assets/javascripts/admin/components/admin-report-stacked-chart.js.es6
@@ -1,3 +1,5 @@
+import { debounce } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { number } from "discourse/lib/formatter";
 import loadScript from "discourse/lib/load-script";
@@ -9,7 +11,7 @@ export default Component.extend({
     this._super(...arguments);
 
     this.resizeHandler = () =>
-      Ember.run.debounce(this, this._scheduleChartRendering, 500);
+      debounce(this, this._scheduleChartRendering, 500);
   },
 
   didInsertElement() {
@@ -29,11 +31,11 @@ export default Component.extend({
   didReceiveAttrs() {
     this._super(...arguments);
 
-    Ember.run.debounce(this, this._scheduleChartRendering, 100);
+    debounce(this, this._scheduleChartRendering, 100);
   },
 
   _scheduleChartRendering() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       if (!this.element) {
         return;
       }
diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6
index 4efdf099263..5b3fbdd9a54 100644
--- a/app/assets/javascripts/admin/components/admin-report.js.es6
+++ b/app/assets/javascripts/admin/components/admin-report.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import ReportLoader from "discourse/lib/reports-loader";
 import { exportEntity } from "discourse/lib/export-csv";
@@ -313,7 +314,7 @@ export default Component.extend({
 
     this.setProperties({ isLoading: true, rateLimitationString: null });
 
-    Ember.run.next(() => {
+    next(() => {
       let payload = this._buildPayload(["prev_period"]);
 
       const callback = response => {
diff --git a/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6
index 81204568218..1dec9fce37b 100644
--- a/app/assets/javascripts/admin/components/admin-theme-editor.js.es6
+++ b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import { default as computed } from "ember-addons/ember-computed-decorators";
 import { fmt } from "discourse/lib/computed";
@@ -83,7 +84,7 @@ export default Component.extend({
 
     toggleMaximize: function() {
       this.toggleProperty("maximized");
-      Ember.run.next(() => this.appEvents.trigger("ace:resize"));
+      next(() => this.appEvents.trigger("ace:resize"));
     },
 
     onlyOverriddenChanged(value) {
diff --git a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6
index 7b6cef45273..f186b291f1e 100644
--- a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6
+++ b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import UserField from "admin/models/user-field";
 import { bufferedProperty } from "discourse/mixins/buffered-content";
@@ -28,7 +29,7 @@ export default Component.extend(bufferedProperty("userField"), {
   @observes("editing")
   _focusOnEdit() {
     if (this.editing) {
-      Ember.run.scheduleOnce("afterRender", this, "_focusName");
+      scheduleOnce("afterRender", this, "_focusName");
     }
   },
 
diff --git a/app/assets/javascripts/admin/components/color-input.js.es6 b/app/assets/javascripts/admin/components/color-input.js.es6
index f6f0025354d..6c02f78dd59 100644
--- a/app/assets/javascripts/admin/components/color-input.js.es6
+++ b/app/assets/javascripts/admin/components/color-input.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { default as loadScript, loadCSS } from "discourse/lib/load-script";
 
@@ -37,7 +38,7 @@ export default Component.extend({
   didInsertElement() {
     loadScript("/javascripts/spectrum.js").then(() => {
       loadCSS("/javascripts/spectrum.css").then(() => {
-        Ember.run.schedule("afterRender", () => {
+        schedule("afterRender", () => {
           $(this.element.querySelector(".picker"))
             .spectrum({ color: "#" + this.hexValue })
             .on("change.spectrum", (me, color) => {
@@ -47,7 +48,7 @@ export default Component.extend({
         });
       });
     });
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this.hexValueChanged();
     });
   }
diff --git a/app/assets/javascripts/admin/components/embeddable-host.js.es6 b/app/assets/javascripts/admin/components/embeddable-host.js.es6
index 746bfeede30..debb6b25b16 100644
--- a/app/assets/javascripts/admin/components/embeddable-host.js.es6
+++ b/app/assets/javascripts/admin/components/embeddable-host.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { bufferedProperty } from "discourse/mixins/buffered-content";
 import computed from "ember-addons/ember-computed-decorators";
@@ -14,7 +15,7 @@ export default Component.extend(bufferedProperty("host"), {
   @on("didInsertElement")
   @observes("editing")
   _focusOnInput() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this.element.querySelector(".host-name").focus();
     });
   },
diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6
index f97deff7113..8438a4ee5c4 100644
--- a/app/assets/javascripts/admin/components/ip-lookup.js.es6
+++ b/app/assets/javascripts/admin/components/ip-lookup.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import { default as computed } from "ember-addons/ember-computed-decorators";
 import { ajax } from "discourse/lib/ajax";
@@ -78,7 +79,7 @@ export default Component.extend({
       $(document.body).append($copyRange);
       if (copyText(text, $copyRange[0])) {
         this.set("copied", true);
-        Ember.run.later(() => this.set("copied", false), 2000);
+        later(() => this.set("copied", false), 2000);
       }
       $copyRange.remove();
     },
diff --git a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js.es6
index dcb3bd3ba59..7fefa0f3e5f 100644
--- a/app/assets/javascripts/admin/components/penalty-post-action.js.es6
+++ b/app/assets/javascripts/admin/components/penalty-post-action.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import computed from "ember-addons/ember-computed-decorators";
 
@@ -23,7 +24,7 @@ export default Component.extend({
 
       // If we switch to edit mode, jump to the edit textarea
       if (postAction === "edit") {
-        Ember.run.scheduleOnce("afterRender", () => {
+        scheduleOnce("afterRender", () => {
           let elem = this.element;
           let body = elem.closest(".modal-body");
           body.scrollTop(body.height());
diff --git a/app/assets/javascripts/admin/components/permalink-form.js.es6 b/app/assets/javascripts/admin/components/permalink-form.js.es6
index 74efe085d96..b1f82ea2958 100644
--- a/app/assets/javascripts/admin/components/permalink-form.js.es6
+++ b/app/assets/javascripts/admin/components/permalink-form.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { default as computed } from "ember-addons/ember-computed-decorators";
 import { fmt } from "discourse/lib/computed";
@@ -20,7 +21,7 @@ export default Component.extend({
   },
 
   focusPermalink() {
-    Ember.run.schedule("afterRender", () =>
+    schedule("afterRender", () =>
       this.element.querySelector(".permalink-url").focus()
     );
   },
@@ -69,7 +70,7 @@ export default Component.extend({
   didInsertElement() {
     this._super(...arguments);
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $(this.element.querySelector(".external-url")).keydown(e => {
         // enter key
         if (e.keyCode === 13) {
diff --git a/app/assets/javascripts/admin/components/resumable-upload.js.es6 b/app/assets/javascripts/admin/components/resumable-upload.js.es6
index 8f6a2913253..344e76deb6d 100644
--- a/app/assets/javascripts/admin/components/resumable-upload.js.es6
+++ b/app/assets/javascripts/admin/components/resumable-upload.js.es6
@@ -1,3 +1,5 @@
+import { schedule } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import { iconHTML } from "discourse-common/lib/icon-library";
 import { bufferedRender } from "discourse-common/lib/buffered-render";
@@ -45,18 +47,18 @@ export default Component.extend(
         this.resumable.upload();
 
         // mark as uploading
-        Ember.run.later(() => this.set("isUploading", true));
+        later(() => this.set("isUploading", true));
       });
 
       this.resumable.on("fileProgress", file => {
         // update progress
-        Ember.run.later(() =>
+        later(() =>
           this.set("progress", parseInt(file.progress() * 100, 10))
         );
       });
 
       this.resumable.on("fileSuccess", file => {
-        Ember.run.later(() => {
+        later(() => {
           // mark as not uploading anymore
           this._reset();
 
@@ -66,7 +68,7 @@ export default Component.extend(
       });
 
       this.resumable.on("fileError", (file, message) => {
-        Ember.run.later(() => {
+        later(() => {
           // mark as not uploading anymore
           this._reset();
 
@@ -78,7 +80,7 @@ export default Component.extend(
 
     @on("didInsertElement")
     _assignBrowse() {
-      Ember.run.schedule("afterRender", () =>
+      schedule("afterRender", () =>
         this.resumable.assignBrowse($(this.element))
       );
     },
@@ -117,7 +119,7 @@ export default Component.extend(
     click() {
       if (this.isUploading) {
         this.resumable.cancel();
-        Ember.run.later(() => this._reset());
+        later(() => this._reset());
         return false;
       } else {
         return true;
diff --git a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6
index 1b20d7f39c0..8b6db577649 100644
--- a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6
+++ b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 /**
   A form to create an IP address that will be blocked or whitelisted.
@@ -62,7 +63,7 @@ export default Component.extend({
           .then(result => {
             this.setProperties({ ip_address: "", formSubmitted: false });
             this.action(ScreenedIpAddress.create(result.screened_ip_address));
-            Ember.run.schedule("afterRender", () =>
+            schedule("afterRender", () =>
               this.element.querySelector(".ip-address-input").focus()
             );
           })
@@ -84,7 +85,7 @@ export default Component.extend({
 
   @on("didInsertElement")
   _init() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $(this.element.querySelector(".ip-address-input")).keydown(e => {
         if (e.keyCode === 13) {
           this.send("submit");
diff --git a/app/assets/javascripts/admin/components/themes-list-item.js.es6 b/app/assets/javascripts/admin/components/themes-list-item.js.es6
index 62e5dff8844..6d68f4d8e3b 100644
--- a/app/assets/javascripts/admin/components/themes-list-item.js.es6
+++ b/app/assets/javascripts/admin/components/themes-list-item.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -33,7 +34,7 @@ export default Component.extend({
   },
 
   scheduleAnimation() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this.animate(true);
     });
   },
diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6
index 919f6de9ef1..74bbeeff1eb 100644
--- a/app/assets/javascripts/admin/components/watched-word-form.js.es6
+++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import WatchedWord from "admin/models/watched-word";
 import {
@@ -64,7 +65,7 @@ export default Component.extend({
               message: I18n.t("admin.watched_words.form.success")
             });
             this.action(WatchedWord.create(result));
-            Ember.run.schedule("afterRender", () =>
+            schedule("afterRender", () =>
               this.element.querySelector(".watched-word-input").focus()
             );
           })
@@ -86,7 +87,7 @@ export default Component.extend({
 
   @on("didInsertElement")
   _init() {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $(this.element.querySelector(".watched-word-input")).keydown(e => {
         if (e.keyCode === 13) {
           this.send("submit");
diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6
index df5d71d3de0..ce99c2cfea4 100644
--- a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import Controller from "@ember/controller";
 import computed from "ember-addons/ember-computed-decorators";
 
@@ -41,7 +42,7 @@ export default Controller.extend({
         );
       }
 
-      Ember.run.later(() => {
+      later(() => {
         this.set("model.savingStatus", null);
       }, 2000);
 
diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6
index 12cc0400f17..b582f733aa3 100644
--- a/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-dashboard-reports.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 import Controller from "@ember/controller";
 import computed from "ember-addons/ember-computed-decorators";
 const { get } = Ember;
@@ -21,7 +22,7 @@ export default Controller.extend({
 
   actions: {
     filterReports(filter) {
-      Ember.run.debounce(this, this._performFiltering, filter, 250);
+      debounce(this, this._performFiltering, filter, 250);
     }
   },
 
diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6
index da3afb72c59..65df52b7f6e 100644
--- a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { scheduleOnce } from "@ember/runloop";
 import Controller from "@ember/controller";
 import { exportEntity } from "discourse/lib/export-csv";
 import { outputExportResult } from "discourse/lib/export-result";
@@ -68,7 +69,7 @@ export default Controller.extend({
   },
 
   scheduleRefresh() {
-    Ember.run.scheduleOnce("afterRender", this, this._refresh);
+    scheduleOnce("afterRender", this, this._refresh);
   },
 
   actions: {
diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6
index 15a16be72b7..1a1b266ae10 100644
--- a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 import Controller from "@ember/controller";
 let lastSearch;
 
@@ -27,14 +28,14 @@ export default Controller.extend({
     toggleOverridden() {
       this.toggleProperty("overridden");
       this.set("searching", true);
-      Ember.run.debounce(this, this._performSearch, 400);
+      debounce(this, this._performSearch, 400);
     },
 
     search() {
       const q = this.q;
       if (q !== lastSearch) {
         this.set("searching", true);
-        Ember.run.debounce(this, this._performSearch, 400);
+        debounce(this, this._performSearch, 400);
         lastSearch = q;
       }
     }
diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6
index 51f1787fe01..eed2c23ec44 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import GrantBadgeController from "discourse/mixins/grant-badge-controller";
@@ -71,7 +72,7 @@ export default Controller.extend(GrantBadgeController, {
       ).then(
         () => {
           this.set("badgeReason", "");
-          Ember.run.next(() => {
+          next(() => {
             // Update the selected badge ID after the combobox has re-rendered.
             const newSelectedBadge = this.grantableBadges[0];
             if (newSelectedBadge) {
diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
index be982921db5..4435eb9b25e 100644
--- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import computed from "ember-addons/ember-computed-decorators";
@@ -51,7 +52,7 @@ export default Controller.extend({
       if (a) {
         a.words.unshiftObject(arg);
         a.incrementProperty("count");
-        Ember.run.schedule("afterRender", () => {
+        schedule("afterRender", () => {
           // remove from other actions lists
           let match = null;
           this.get("adminWatchedWords.model").forEach(action => {
diff --git a/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6 b/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6
index 8e23ba55ee9..d3d301212ae 100644
--- a/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6
+++ b/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 // Ember 2.0 removes buffered rendering, but we can still implement it ourselves.
 // In the long term we'll want to remove this.
 
@@ -13,7 +14,7 @@ const Mixin = {
   },
 
   rerenderBuffer() {
-    Ember.run.scheduleOnce("render", this, this._customRender);
+    scheduleOnce("render", this, this._customRender);
   }
 };
 
diff --git a/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6 b/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6
index 3f70e353b89..974d73c958a 100644
--- a/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6
+++ b/app/assets/javascripts/discourse-common/mixins/focus-event.js.es6
@@ -1,10 +1,11 @@
+import { bind } from "@ember/runloop";
 import { getOwner } from "discourse-common/lib/get-owner";
 
 export default Ember.Mixin.create({
   ready() {
     this._super(...arguments);
 
-    this._onChangeHandler = Ember.run.bind(this, this._onChange);
+    this._onChangeHandler = bind(this, this._onChange);
 
     // Default to true
     Discourse.set("hasFocus", true);
diff --git a/app/assets/javascripts/discourse-loader.js b/app/assets/javascripts/discourse-loader.js
index 1762d116aac..52ad20808c3 100644
--- a/app/assets/javascripts/discourse-loader.js
+++ b/app/assets/javascripts/discourse-loader.js
@@ -14,6 +14,18 @@ var define, requirejs;
       "@ember/object": { default: Ember.Object },
       "@ember/object/proxy": { default: Ember.ObjectProxy },
       "@ember/routing/route": { default: Ember.Route },
+      "@ember/runloop": {
+        bind: Ember.run.bind,
+        cancel: Ember.run.cancel,
+        debounce: Ember.run.debounce,
+        later: Ember.run.later,
+        next: Ember.run.next,
+        once: Ember.run.once,
+        run: Ember.run,
+        schedule: Ember.run.schedule,
+        scheduleOnce: Ember.run.scheduleOnce,
+        throttle: Ember.run.throttle
+      },
       "@ember/service": {
         default: Ember.Service,
         inject: Ember.inject.service
diff --git a/app/assets/javascripts/discourse/components/add-category-tag-classes.js.es6 b/app/assets/javascripts/discourse/components/add-category-tag-classes.js.es6
index 209367e8298..d4a75e6ebe3 100644
--- a/app/assets/javascripts/discourse/components/add-category-tag-classes.js.es6
+++ b/app/assets/javascripts/discourse/components/add-category-tag-classes.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { observes } from "ember-addons/ember-computed-decorators";
 
@@ -26,7 +27,7 @@ export default Component.extend({
 
   @observes("category.fullSlug", "tags")
   refreshClass() {
-    Ember.run.scheduleOnce("afterRender", this, this._updateClass);
+    scheduleOnce("afterRender", this, this._updateClass);
   },
 
   _removeClass() {
diff --git a/app/assets/javascripts/discourse/components/choose-message.js.es6 b/app/assets/javascripts/discourse/components/choose-message.js.es6
index b4518c2c4f6..c6dd177fe9f 100644
--- a/app/assets/javascripts/discourse/components/choose-message.js.es6
+++ b/app/assets/javascripts/discourse/components/choose-message.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import debounce from "discourse/lib/debounce";
 import { searchForTerm } from "discourse/lib/search";
@@ -57,7 +58,7 @@ export default Component.extend({
     chooseMessage(message) {
       const messageId = Ember.get(message, "id");
       this.set("selectedTopicId", messageId);
-      Ember.run.next(() =>
+      next(() =>
         $(`#choose-message-${messageId}`).prop("checked", "true")
       );
       return false;
diff --git a/app/assets/javascripts/discourse/components/choose-topic.js.es6 b/app/assets/javascripts/discourse/components/choose-topic.js.es6
index df0d3b1c476..a31aa5eee4c 100644
--- a/app/assets/javascripts/discourse/components/choose-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/choose-topic.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import debounce from "discourse/lib/debounce";
 import { searchForTerm } from "discourse/lib/search";
@@ -62,7 +63,7 @@ export default Component.extend({
   actions: {
     chooseTopic(topic) {
       this.set("selectedTopicId", topic.id);
-      Ember.run.next(() => {
+      next(() => {
         document.getElementById(`choose-topic-${topic.id}`).checked = true;
       });
       return false;
diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6
index 7fea43389d2..f265917279b 100644
--- a/app/assets/javascripts/discourse/components/composer-body.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-body.js.es6
@@ -1,3 +1,8 @@
+import { throttle } from "@ember/runloop";
+import { run } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -60,7 +65,7 @@ export default Component.extend(KeyEnterEscape, {
     "composer.canEditTopicFeaturedLink"
   )
   resize() {
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       if (!this.element || this.isDestroying || this.isDestroyed) {
         return;
       }
@@ -78,8 +83,8 @@ export default Component.extend(KeyEnterEscape, {
 
     // One second from now, check to see if the last key was hit when
     // we recorded it. If it was, the user paused typing.
-    Ember.run.cancel(this._lastKeyTimeout);
-    this._lastKeyTimeout = Ember.run.later(() => {
+    cancel(this._lastKeyTimeout);
+    this._lastKeyTimeout = later(() => {
       if (lastKeyUp !== this._lastKeyUp) {
         return;
       }
@@ -116,7 +121,7 @@ export default Component.extend(KeyEnterEscape, {
 
     const throttledPerformDrag = (event => {
       event.preventDefault();
-      Ember.run.throttle(this, performDrag, event, THROTTLE_RATE);
+      throttle(this, performDrag, event, THROTTLE_RATE);
     }).bind(this);
 
     const endDrag = () => {
@@ -153,7 +158,7 @@ export default Component.extend(KeyEnterEscape, {
     this._super(...arguments);
     this.setupComposerResizeEvents();
 
-    const resize = () => Ember.run(() => this.resize());
+    const resize = () => run(() => this.resize());
     const triggerOpen = () => {
       if (this.get("composer.composeState") === Composer.OPEN) {
         this.appEvents.trigger("composer:opened");
diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index cc419e1af90..c9ccf82fa6d 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -1,3 +1,8 @@
+import { throttle } from "@ember/runloop";
+import { next } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import userSearch from "discourse/lib/user-search";
 import {
@@ -183,7 +188,7 @@ export default Component.extend({
         transformComplete: v => v.username || v.name,
         afterComplete() {
           // ensures textarea scroll position is correct
-          Ember.run.scheduleOnce("afterRender", () => $input.blur().focus());
+          scheduleOnce("afterRender", () => $input.blur().focus());
         }
       });
     }
@@ -192,7 +197,7 @@ export default Component.extend({
       this._initInputPreviewSync($input, $preview);
     } else {
       $input.on("scroll", () =>
-        Ember.run.throttle(
+        throttle(
           this,
           this._syncEditorAndPreviewScroll,
           $input,
@@ -316,7 +321,7 @@ export default Component.extend({
       this.appEvents.on(event, this, this._resetShouldBuildScrollMap);
     });
 
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       $input.on("touchstart mouseenter", () => {
         if (!$preview.is(":visible")) return;
         $preview.off("scroll");
@@ -342,7 +347,7 @@ export default Component.extend({
       this.set("shouldBuildScrollMap", false);
     }
 
-    Ember.run.throttle(this, $callback, $input, $preview, this.scrollMap, 20);
+    throttle(this, $callback, $input, $preview, this.scrollMap, 20);
   },
 
   _teardownInputPreviewSync() {
@@ -559,7 +564,7 @@ export default Component.extend({
   },
 
   _warnMentionedGroups($preview) {
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       var found = this.warnedGroupMentions || [];
       $preview.find(".mention-group.notify").each((idx, e) => {
         const $e = $(e);
@@ -587,7 +592,7 @@ export default Component.extend({
       return;
     }
 
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       let found = this.warnedCannotSeeMentions || [];
 
       $preview.find(".mention.cannot-see").each((idx, e) => {
@@ -597,7 +602,7 @@ export default Component.extend({
         if (found.indexOf(name) === -1) {
           // add a delay to allow for typing, so you don't open the warning right away
           // previously we would warn after @bob even if you were about to mention @bob2
-          Ember.run.later(
+          later(
             this,
             () => {
               if (
@@ -618,7 +623,7 @@ export default Component.extend({
   },
 
   _resetUpload(removePlaceholder) {
-    Ember.run.next(() => {
+    next(() => {
       if (this._validUploads > 0) {
         this._validUploads--;
       }
@@ -897,9 +902,9 @@ export default Component.extend({
   @on("willDestroyElement")
   _composerClosed() {
     this.appEvents.trigger("composer:will-close");
-    Ember.run.next(() => {
+    next(() => {
       // need to wait a bit for the "slide down" transition of the composer
-      Ember.run.later(
+      later(
         () => this.appEvents.trigger("composer:closed"),
         Ember.testing ? 0 : 400
       );
@@ -970,7 +975,7 @@ export default Component.extend({
       // Paint mentions
       const unseenMentions = linkSeenMentions($preview, this.siteSettings);
       if (unseenMentions.length) {
-        Ember.run.debounce(
+        debounce(
           this,
           this._renderUnseenMentions,
           $preview,
@@ -985,7 +990,7 @@ export default Component.extend({
       // Paint category hashtags
       const unseenCategoryHashtags = linkSeenCategoryHashtags($preview);
       if (unseenCategoryHashtags.length) {
-        Ember.run.debounce(
+        debounce(
           this,
           this._renderUnseenCategoryHashtags,
           $preview,
@@ -998,7 +1003,7 @@ export default Component.extend({
       if (this.siteSettings.tagging_enabled) {
         const unseenTagHashtags = linkSeenTagHashtags($preview);
         if (unseenTagHashtags.length) {
-          Ember.run.debounce(
+          debounce(
             this,
             this._renderUnseenTagHashtags,
             $preview,
@@ -1009,7 +1014,7 @@ export default Component.extend({
       }
 
       // Paint oneboxes
-      Ember.run.debounce(
+      debounce(
         this,
         () => {
           const oneboxes = {};
diff --git a/app/assets/javascripts/discourse/components/composer-messages.js.es6 b/app/assets/javascripts/discourse/components/composer-messages.js.es6
index fb07997b809..82a4fceeaf8 100644
--- a/app/assets/javascripts/discourse/components/composer-messages.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-messages.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import LinkLookup from "discourse/lib/link-lookup";
 
@@ -24,7 +25,7 @@ export default Component.extend({
     this.appEvents.on("composer:find-similar", this, this._findSimilar);
     this.appEvents.on("composer-messages:close", this, this._closeTop);
     this.appEvents.on("composer-messages:create", this, this._create);
-    Ember.run.scheduleOnce("afterRender", this, this.reset);
+    scheduleOnce("afterRender", this, this.reset);
   },
 
   willDestroyElement() {
diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6
index 459d1bd0a6e..540f1b36eb1 100644
--- a/app/assets/javascripts/discourse/components/composer-title.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-title.js.es6
@@ -1,3 +1,6 @@
+import { next } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -24,7 +27,7 @@ export default Component.extend({
     }
 
     if (this.get("composer.titleLength") > 0) {
-      Ember.run.debounce(this, this._titleChanged, 10);
+      debounce(this, this._titleChanged, 10);
     }
   },
 
@@ -79,13 +82,13 @@ export default Component.extend({
     }
 
     if (Ember.testing) {
-      Ember.run.next(() =>
+      next(() =>
         // not ideal but we don't want to run this in current
         // runloop to avoid an error in console
         this._checkForUrl()
       );
     } else {
-      Ember.run.debounce(this, this._checkForUrl, 500);
+      debounce(this, this._checkForUrl, 500);
     }
   },
 
@@ -134,14 +137,14 @@ export default Component.extend({
           })
           .finally(() => {
             this.set("composer.loading", false);
-            Ember.run.schedule("afterRender", () => {
+            schedule("afterRender", () => {
               $(this.element.querySelector("input")).putCursorAtEnd();
             });
           });
       } else {
         this._updatePost(loadOnebox);
         this.set("composer.loading", false);
-        Ember.run.schedule("afterRender", () => {
+        schedule("afterRender", () => {
           $(this.element.querySelector("input")).putCursorAtEnd();
         });
       }
diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
index 694db1cfd37..3e9aa03784c 100644
--- a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -76,7 +77,7 @@ export default Component.extend({
     toggleSelector() {
       this.set("showSelector", true);
 
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         $(this.element)
           .find("input")
           .focus();
diff --git a/app/assets/javascripts/discourse/components/cook-text.js.es6 b/app/assets/javascripts/discourse/components/cook-text.js.es6
index e9e19b338fc..256636a3c58 100644
--- a/app/assets/javascripts/discourse/components/cook-text.js.es6
+++ b/app/assets/javascripts/discourse/components/cook-text.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import { cookAsync } from "discourse/lib/text";
 import { ajax } from "discourse/lib/ajax";
@@ -12,7 +13,7 @@ const CookText = Component.extend({
       this.set("cooked", cooked);
       // no choice but to defer this cause
       // pretty text may only be loaded now
-      Ember.run.next(() =>
+      next(() =>
         window
           .requireModule("pretty-text/upload-short-url")
           .resolveAllShortUrls(ajax)
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index 185223be34e..4b95ddc8142 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -1,3 +1,8 @@
+import { next } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import { inject as service } from "@ember/service";
 import Component from "@ember/component";
 /*global Mousetrap:true */
@@ -249,7 +254,7 @@ export default Component.extend({
     this._applyEmojiAutocomplete($editorInput);
     this._applyCategoryHashtagAutocomplete($editorInput);
 
-    Ember.run.scheduleOnce("afterRender", this, this._readyNow);
+    scheduleOnce("afterRender", this, this._readyNow);
 
     const mouseTrap = Mousetrap(this.element.querySelector(".d-editor-input"));
     const shortcuts = this.get("toolbar.shortcuts");
@@ -348,7 +353,7 @@ export default Component.extend({
         return;
       }
       this.set("preview", cooked);
-      Ember.run.scheduleOnce("afterRender", () => {
+      scheduleOnce("afterRender", () => {
         if (this._state !== "inDOM") {
           return;
         }
@@ -372,7 +377,7 @@ export default Component.extend({
     if (Ember.testing) {
       this._updatePreview();
     } else {
-      Ember.run.debounce(this, this._updatePreview, 30);
+      debounce(this, this._updatePreview, 30);
     }
   },
 
@@ -432,14 +437,14 @@ export default Component.extend({
             emojiPickerIsActive: true
           });
 
-          Ember.run.schedule("afterRender", () => {
+          schedule("afterRender", () => {
             const filterInput = document.querySelector(
               ".emoji-picker input[name='filter']"
             );
             if (filterInput) {
               filterInput.value = v.term;
 
-              Ember.run.later(
+              later(
                 () => filterInput.dispatchEvent(new Event("input")),
                 50
               );
@@ -547,7 +552,7 @@ export default Component.extend({
   },
 
   _selectText(from, length) {
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       const textarea = this.element.querySelector("textarea.d-editor-input");
       const $textarea = $(textarea);
       const oldScrollPos = $textarea.scrollTop();
@@ -556,7 +561,7 @@ export default Component.extend({
       }
       textarea.selectionStart = from;
       textarea.selectionEnd = from + length;
-      Ember.run.next(() => $textarea.trigger("change"));
+      next(() => $textarea.trigger("change"));
       $textarea.scrollTop(oldScrollPos);
     });
   },
@@ -785,7 +790,7 @@ export default Component.extend({
     $textarea.val(value);
     $textarea.prop("selectionStart", insert.length);
     $textarea.prop("selectionEnd", insert.length);
-    Ember.run.next(() => $textarea.trigger("change"));
+    next(() => $textarea.trigger("change"));
     this._focusTextArea();
   },
 
@@ -886,7 +891,7 @@ export default Component.extend({
   // ensures textarea scroll position is correct
   _focusTextArea() {
     const textarea = this.element.querySelector("textarea.d-editor-input");
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       textarea.blur();
       textarea.focus();
     });
diff --git a/app/assets/javascripts/discourse/components/d-modal-body.js.es6 b/app/assets/javascripts/discourse/components/d-modal-body.js.es6
index 33b444d05b3..a8543095abb 100644
--- a/app/assets/javascripts/discourse/components/d-modal-body.js.es6
+++ b/app/assets/javascripts/discourse/components/d-modal-body.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 export default Component.extend({
   classNames: ["modal-body"],
@@ -14,7 +15,7 @@ export default Component.extend({
       fixedParent.modal("show");
     }
 
-    Ember.run.scheduleOnce("afterRender", this, this._afterFirstRender);
+    scheduleOnce("afterRender", this, this._afterFirstRender);
     this.appEvents.on("modal-body:flash", this, "_flash");
     this.appEvents.on("modal-body:clearFlash", this, "_clearFlash");
   },
diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6
index 0c83f529ad9..19d4d2264c5 100644
--- a/app/assets/javascripts/discourse/components/d-modal.js.es6
+++ b/app/assets/javascripts/discourse/components/d-modal.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { on } from "ember-addons/ember-computed-decorators";
 import Component from "@ember/component";
 
@@ -32,11 +33,11 @@ export default Component.extend({
   setUp() {
     $("html").on("keydown.discourse-modal", e => {
       if (e.which === 27 && this.dismissable) {
-        Ember.run.next(() => $(".modal-header button.modal-close").click());
+        next(() => $(".modal-header button.modal-close").click());
       }
 
       if (e.which === 13 && this.triggerClickOnEnter(e)) {
-        Ember.run.next(() => $(".modal-footer .btn-primary").click());
+        next(() => $(".modal-footer .btn-primary").click());
       }
     });
 
diff --git a/app/assets/javascripts/discourse/components/date-input.js.es6 b/app/assets/javascripts/discourse/components/date-input.js.es6
index 0161fb8aa21..927be37881a 100644
--- a/app/assets/javascripts/discourse/components/date-input.js.es6
+++ b/app/assets/javascripts/discourse/components/date-input.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 /* global Pikaday:true */
 import loadScript from "discourse/lib/load-script";
@@ -37,7 +38,7 @@ export default Component.extend({
 
   _loadPikadayPicker(container) {
     loadScript("/javascripts/pikaday.js").then(() => {
-      Ember.run.next(() => {
+      next(() => {
         const default_opts = {
           field: this.element.querySelector(".date-picker"),
           container: container || this.element,
diff --git a/app/assets/javascripts/discourse/components/date-picker.js.es6 b/app/assets/javascripts/discourse/components/date-picker.js.es6
index 7fc28a50ef2..e9403326bbd 100644
--- a/app/assets/javascripts/discourse/components/date-picker.js.es6
+++ b/app/assets/javascripts/discourse/components/date-picker.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 /* global Pikaday:true */
 import loadScript from "discourse/lib/load-script";
@@ -30,7 +31,7 @@ export default Component.extend({
 
   _loadPikadayPicker(container) {
     loadScript("/javascripts/pikaday.js").then(() => {
-      Ember.run.next(() => {
+      next(() => {
         const options = {
           field: this.element.querySelector(".date-picker"),
           container: container || null,
diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
index 53ab8edbbe3..796e0a90683 100644
--- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6
+++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6
@@ -1,3 +1,7 @@
+import { throttle } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import DiscourseURL from "discourse/lib/url";
 import AddArchetypeClass from "discourse/mixins/add-archetype-class";
@@ -50,13 +54,13 @@ export default Component.extend(
       const enteredAt = this.enteredAt;
       if (enteredAt && this.lastEnteredAt !== enteredAt) {
         this._lastShowTopic = null;
-        Ember.run.schedule("afterRender", () => this.scrolled());
+        schedule("afterRender", () => this.scrolled());
         this.set("lastEnteredAt", enteredAt);
       }
     },
 
     _highlightPost(postNumber) {
-      Ember.run.scheduleOnce("afterRender", null, highlight, postNumber);
+      scheduleOnce("afterRender", null, highlight, postNumber);
     },
 
     _hideTopicInHeader() {
@@ -78,7 +82,7 @@ export default Component.extend(
           this.pauseHeaderTopicUpdate = true;
           this._lastShowTopic = true;
 
-          Ember.run.later(() => {
+          later(() => {
             this._lastShowTopic = false;
             this.pauseHeaderTopicUpdate = false;
           }, debounceDuration);
@@ -192,7 +196,7 @@ export default Component.extend(
       // at the start of the scroll. This feels a lot more snappy compared to waiting
       // for the scroll to end if we debounce.
       if (this.site.mobileView && this.hasScrolled) {
-        Ember.run.throttle(
+        throttle(
           this,
           this.calculateDirection,
           offset,
diff --git a/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6 b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
index c9b2bb92bb9..9c6cb779c24 100644
--- a/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
+++ b/app/assets/javascripts/discourse/components/discovery-topics-list.js.es6
@@ -1,3 +1,5 @@
+import { schedule } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { on, observes } from "ember-addons/ember-computed-decorators";
 import LoadMore from "discourse/mixins/load-more";
@@ -12,11 +14,11 @@ const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
   _readjustScrollPosition() {
     const scrollTo = this.session.get("topicListScrollPosition");
     if (scrollTo && scrollTo >= 0) {
-      Ember.run.schedule("afterRender", () =>
+      schedule("afterRender", () =>
         $(window).scrollTop(scrollTo + 1)
       );
     } else {
-      Ember.run.scheduleOnce("afterRender", this, this.loadMoreUnlessFull);
+      scheduleOnce("afterRender", this, this.loadMoreUnlessFull);
     }
   },
 
@@ -38,7 +40,7 @@ const DiscoveryTopicsListComponent = Component.extend(UrlRefresh, LoadMore, {
     loadMore() {
       Discourse.updateContextCount(0);
       this.model.loadMore().then(hasMoreResults => {
-        Ember.run.schedule("afterRender", () => this.saveScrollPosition());
+        schedule("afterRender", () => this.saveScrollPosition());
         if (!hasMoreResults) {
           this.eyeline.flushRest();
         } else if ($(window).height() >= $(document).height()) {
diff --git a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
index 15aae0ed284..42be7233afb 100644
--- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { propertyEqual } from "discourse/lib/computed";
 import computed from "ember-addons/ember-computed-decorators";
@@ -20,7 +21,7 @@ export default Component.extend({
 
   didInsertElement() {
     this._super(...arguments);
-    Ember.run.scheduleOnce("afterRender", this, this._addToCollection);
+    scheduleOnce("afterRender", this, this._addToCollection);
   },
 
   _addToCollection: function() {
diff --git a/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6 b/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
index b1d5c79d812..c3254cc3d6e 100644
--- a/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-category-topic-template.js.es6
@@ -1,9 +1,10 @@
+import { scheduleOnce } from "@ember/runloop";
 import { buildCategoryPanel } from "discourse/components/edit-category-panel";
 
 export default buildCategoryPanel("topic-template", {
   _activeTabChanged: function() {
     if (this.activeTab) {
-      Ember.run.scheduleOnce("afterRender", () =>
+      scheduleOnce("afterRender", () =>
         this.element.querySelector(".d-editor-input").focus()
       );
     }
diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
index 5d01c739e7e..c46af80e2f6 100644
--- a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
+++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -73,7 +74,7 @@ export default Component.extend({
   @observes("selection")
   _updateBasedOnLastPost() {
     if (!this.autoClose) {
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         this.set("topicTimer.based_on_last_post", false);
       });
     }
diff --git a/app/assets/javascripts/discourse/components/expanding-text-area.js.es6 b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
index 7008eb1d013..d2b980a8f23 100644
--- a/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
+++ b/app/assets/javascripts/discourse/components/expanding-text-area.js.es6
@@ -1,10 +1,11 @@
+import { scheduleOnce } from "@ember/runloop";
 import { on, observes } from "ember-addons/ember-computed-decorators";
 import autosize from "discourse/lib/autosize";
 
 export default Ember.TextArea.extend({
   @on("didInsertElement")
   _startWatching() {
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       $(this.element).focus();
       autosize(this.element);
     });
diff --git a/app/assets/javascripts/discourse/components/flag-selection.js.es6 b/app/assets/javascripts/discourse/components/flag-selection.js.es6
index f039b306600..b52f5449078 100644
--- a/app/assets/javascripts/discourse/components/flag-selection.js.es6
+++ b/app/assets/javascripts/discourse/components/flag-selection.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import { observes } from "ember-addons/ember-computed-decorators";
 
@@ -16,6 +17,6 @@ export default Component.extend({
 
   @observes("nameKey")
   selectedChanged() {
-    Ember.run.next(this, this._selectRadio);
+    next(this, this._selectRadio);
   }
 });
diff --git a/app/assets/javascripts/discourse/components/footer-nav.js.es6 b/app/assets/javascripts/discourse/components/footer-nav.js.es6
index 7ac143225a1..d94910364ec 100644
--- a/app/assets/javascripts/discourse/components/footer-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/footer-nav.js.es6
@@ -1,3 +1,4 @@
+import { throttle } from "@ember/runloop";
 import MountWidget from "discourse/components/mount-widget";
 import MobileScrollDirection from "discourse/mixins/mobile-scroll-direction";
 import Scrolling from "discourse/mixins/scrolling";
@@ -78,7 +79,7 @@ const FooterNavComponent = MountWidget.extend(
 
       const offset = window.pageYOffset || $("html").scrollTop();
 
-      Ember.run.throttle(
+      throttle(
         this,
         this.calculateDirection,
         offset,
diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6
index 5bacd98013e..571d895f84b 100644
--- a/app/assets/javascripts/discourse/components/global-notice.js.es6
+++ b/app/assets/javascripts/discourse/components/global-notice.js.es6
@@ -1,3 +1,4 @@
+import { bind } from "@ember/runloop";
 import Component from "@ember/component";
 import { on } from "ember-addons/ember-computed-decorators";
 import { iconHTML } from "discourse-common/lib/icon-library";
@@ -88,10 +89,10 @@ export default Component.extend(
 
     @on("didInsertElement")
     _setupLogsNotice() {
-      this._boundRerenderBuffer = Ember.run.bind(this, this.rerenderBuffer);
+      this._boundRerenderBuffer = bind(this, this.rerenderBuffer);
       LogsNotice.current().addObserver("hidden", this._boundRerenderBuffer);
 
-      this._boundResetCurrentProp = Ember.run.bind(
+      this._boundResetCurrentProp = bind(
         this,
         this._resetCurrentProp
       );
diff --git a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6 b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
index a80512b93d7..a190ffda1db 100644
--- a/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
+++ b/app/assets/javascripts/discourse/components/group-flair-inputs.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 import Component from "@ember/component";
 import computed from "ember-addons/ember-computed-decorators";
 import { observes } from "ember-addons/ember-computed-decorators";
@@ -25,7 +26,7 @@ export default Component.extend({
 
   @observes("model.flair_url")
   _loadSVGIcon() {
-    Ember.run.debounce(this, this._loadIcon, 1000);
+    debounce(this, this._loadIcon, 1000);
   },
 
   _loadIcon() {
diff --git a/app/assets/javascripts/discourse/components/image-uploader.js.es6 b/app/assets/javascripts/discourse/components/image-uploader.js.es6
index 9142c17e60e..ca919959a8a 100644
--- a/app/assets/javascripts/discourse/components/image-uploader.js.es6
+++ b/app/assets/javascripts/discourse/components/image-uploader.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import computed from "ember-addons/ember-computed-decorators";
 import UploadMixin from "discourse/mixins/upload";
@@ -77,13 +78,13 @@ export default Component.extend(UploadMixin, {
   },
 
   _openLightbox() {
-    Ember.run.next(() =>
+    next(() =>
       $(this.element.querySelector("a.lightbox")).magnificPopup("open")
     );
   },
 
   _applyLightbox() {
-    if (this.imageUrl) Ember.run.next(() => lightbox($(this.element)));
+    if (this.imageUrl) next(() => lightbox($(this.element)));
   },
 
   actions: {
diff --git a/app/assets/javascripts/discourse/components/link-to-input.js.es6 b/app/assets/javascripts/discourse/components/link-to-input.js.es6
index 653761018eb..19015ec0e8f 100644
--- a/app/assets/javascripts/discourse/components/link-to-input.js.es6
+++ b/app/assets/javascripts/discourse/components/link-to-input.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 export default Component.extend({
   showInput: false,
@@ -5,7 +6,7 @@ export default Component.extend({
   click() {
     this.onClick();
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $(this.element)
         .find("input")
         .focus();
diff --git a/app/assets/javascripts/discourse/components/login-modal.js.es6 b/app/assets/javascripts/discourse/components/login-modal.js.es6
index e8bbe98c876..2ee72f87225 100644
--- a/app/assets/javascripts/discourse/components/login-modal.js.es6
+++ b/app/assets/javascripts/discourse/components/login-modal.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 export default Component.extend({
   didInsertElement() {
@@ -14,7 +15,7 @@ export default Component.extend({
       this.set("loginName", $.cookie("email"));
     }
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $(
         "#login-account-password, #login-account-name, #login-second-factor"
       ).keydown(e => {
diff --git a/app/assets/javascripts/discourse/components/mobile-nav.js.es6 b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
index a60a1070794..a47c09c5c47 100644
--- a/app/assets/javascripts/discourse/components/mobile-nav.js.es6
+++ b/app/assets/javascripts/discourse/components/mobile-nav.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import { on, observes } from "ember-addons/ember-computed-decorators";
 
@@ -21,7 +22,7 @@ export default Component.extend({
   @observes("currentPath")
   currentPathChanged() {
     this.set("expanded", false);
-    Ember.run.next(() => this._updateSelectedHtml());
+    next(() => this._updateSelectedHtml());
   },
 
   _updateSelectedHtml() {
@@ -41,7 +42,7 @@ export default Component.extend({
     toggleExpanded() {
       this.toggleProperty("expanded");
 
-      Ember.run.next(() => {
+      next(() => {
         if (this.expanded) {
           $(window)
             .off("click.mobile-nav")
diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6
index 5f0e584977b..1363750b81d 100644
--- a/app/assets/javascripts/discourse/components/mount-widget.js.es6
+++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6
@@ -1,3 +1,5 @@
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { diff, patch } from "virtual-dom";
 import { WidgetClickHook } from "discourse/widgets/hooks";
@@ -50,7 +52,7 @@ export default Component.extend({
 
     this._rootNode = document.createElement("div");
     this.element.appendChild(this._rootNode);
-    this._timeout = Ember.run.scheduleOnce("render", this, this.rerenderWidget);
+    this._timeout = scheduleOnce("render", this, this.rerenderWidget);
   },
 
   willClearRender() {
@@ -68,7 +70,7 @@ export default Component.extend({
       const [eventName, caller] = evt;
       this.appEvents.off(eventName, this, caller);
     });
-    Ember.run.cancel(this._timeout);
+    cancel(this._timeout);
   },
 
   afterRender() {},
@@ -97,13 +99,13 @@ export default Component.extend({
       this._renderCallback = callback;
     }
 
-    Ember.run.scheduleOnce("render", this, this.rerenderWidget);
+    scheduleOnce("render", this, this.rerenderWidget);
   },
 
   buildArgs() {},
 
   rerenderWidget() {
-    Ember.run.cancel(this._timeout);
+    cancel(this._timeout);
 
     if (this._rootNode) {
       if (!this._widgetClass) {
diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
index 01dacde789d..bf2947fc1db 100644
--- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6
+++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -69,7 +70,7 @@ export default Component.extend({
       if (this.expanded) {
         DiscourseURL.appEvents.on("dom:clean", this, this.ensureDropClosed);
 
-        Ember.run.next(() => {
+        next(() => {
           if (!this.expanded) {
             return;
           }
@@ -77,7 +78,7 @@ export default Component.extend({
           $(this.element.querySelector(".drop a")).on("click", () => {
             this.element.querySelector(".drop").style.display = "none";
 
-            Ember.run.next(() => {
+            next(() => {
               if (!this.element || this.isDestroying || this.isDestroyed) {
                 return;
               }
diff --git a/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6 b/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
index b6c9df818bb..a82f76518b0 100644
--- a/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
+++ b/app/assets/javascripts/discourse/components/pwa-install-banner.js.es6
@@ -1,3 +1,4 @@
+import { bind } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -18,7 +19,7 @@ export default Component.extend({
 
   @on("didInsertElement")
   _registerListener() {
-    this._promptEventHandler = Ember.run.bind(
+    this._promptEventHandler = bind(
       this,
       this._handleInstallPromptEvent
     );
diff --git a/app/assets/javascripts/discourse/components/quote-button.js.es6 b/app/assets/javascripts/discourse/components/quote-button.js.es6
index f16bf6690d7..ec8b8c47e88 100644
--- a/app/assets/javascripts/discourse/components/quote-button.js.es6
+++ b/app/assets/javascripts/discourse/components/quote-button.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import debounce from "discourse/lib/debounce";
 import { selectedText } from "discourse/lib/utilities";
@@ -103,7 +104,7 @@ export default Component.extend({
     }
 
     // change the position of the button
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       let top = markerOffset.top;
       let left = markerOffset.left + Math.max(0, parentScrollLeft);
 
diff --git a/app/assets/javascripts/discourse/components/scroll-tracker.js.es6 b/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
index dad7447f0c2..7637845c1e3 100644
--- a/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
+++ b/app/assets/javascripts/discourse/components/scroll-tracker.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import Scrolling from "discourse/mixins/scrolling";
 
@@ -19,7 +20,7 @@ export default Component.extend(Scrolling, {
 
     const data = this.session.get(this.trackerName);
     if (data && data.position >= 0 && data.tag === this.tag) {
-      Ember.run.next(() => $(window).scrollTop(data.position + 1));
+      next(() => $(window).scrollTop(data.position + 1));
     }
   },
 
diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
index 89a48126f30..38f6536b9d4 100644
--- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6
@@ -1,3 +1,6 @@
+import { next } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import DiscourseURL from "discourse/lib/url";
 import MountWidget from "discourse/components/mount-widget";
 import { cloak, uncloak } from "discourse/widgets/post-stream";
@@ -204,7 +207,7 @@ export default MountWidget.extend({
               // will cause the browser to scroll to the top of the document
               // in Chrome. This makes sure the scroll works correctly if that
               // happens.
-              Ember.run.next(() => $("html, body").scrollTop(whereY));
+              next(() => $("html, body").scrollTop(whereY));
             }
           });
         };
@@ -270,7 +273,7 @@ export default MountWidget.extend({
   },
 
   _scrollTriggered() {
-    Ember.run.scheduleOnce("afterRender", this, this.scrolled);
+    scheduleOnce("afterRender", this, this.scrolled);
   },
 
   _posted(staged) {
@@ -306,13 +309,13 @@ export default MountWidget.extend({
   },
 
   _debouncedScroll() {
-    Ember.run.debounce(this, this._scrollTriggered, 10);
+    debounce(this, this._scrollTriggered, 10);
   },
 
   didInsertElement() {
     this._super(...arguments);
     const debouncedScroll = () =>
-      Ember.run.debounce(this, this._scrollTriggered, 10);
+      debounce(this, this._scrollTriggered, 10);
 
     this._previouslyNearby = {};
 
diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
index 333b6d65492..8f1779487d2 100644
--- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
+++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6
@@ -1,3 +1,5 @@
+import { debounce } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { observes } from "ember-addons/ember-computed-decorators";
 import { escapeExpression } from "discourse/lib/utilities";
@@ -72,13 +74,13 @@ export default Component.extend({
 
     this._init();
 
-    Ember.run.scheduleOnce("afterRender", () => this._update());
+    scheduleOnce("afterRender", () => this._update());
   },
 
   @observes("searchTerm")
   _updateOptions() {
     this._update();
-    Ember.run.debounce(this, this._update, 250);
+    debounce(this, this._update, 250);
   },
 
   _init() {
diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6
index 65f5cfd9641..45b9317145d 100644
--- a/app/assets/javascripts/discourse/components/share-panel.js.es6
+++ b/app/assets/javascripts/discourse/components/share-panel.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { escapeExpression } from "discourse/lib/utilities";
 import { default as computed } from "ember-addons/ember-computed-decorators";
@@ -47,7 +48,7 @@ export default Component.extend({
       this.element.querySelector(".topic-share-url-for-touch a")
     );
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       if (!this.capabilities.touch) {
         $linkForTouch.parent().remove();
 
diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6
index 17e05c47c97..49d8f677235 100644
--- a/app/assets/javascripts/discourse/components/share-popup.js.es6
+++ b/app/assets/javascripts/discourse/components/share-popup.js.es6
@@ -1,3 +1,5 @@
+import { bind } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 import { longDateNoYear } from "discourse/lib/formatter";
@@ -89,7 +91,7 @@ export default Component.extend({
     this.set("link", url);
     this.set("visible", true);
 
-    Ember.run.scheduleOnce("afterRender", this, this._focusUrl);
+    scheduleOnce("afterRender", this, this._focusUrl);
   },
 
   _mouseDownHandler(event) {
@@ -154,9 +156,9 @@ export default Component.extend({
 
   @on("init")
   _setupHandlers() {
-    this._boundMouseDownHandler = Ember.run.bind(this, this._mouseDownHandler);
-    this._boundClickHandler = Ember.run.bind(this, this._clickHandler);
-    this._boundKeydownHandler = Ember.run.bind(this, this._keydownHandler);
+    this._boundMouseDownHandler = bind(this, this._mouseDownHandler);
+    this._boundClickHandler = bind(this, this._clickHandler);
+    this._boundKeydownHandler = bind(this, this._keydownHandler);
   },
 
   didInsertElement() {
diff --git a/app/assets/javascripts/discourse/components/signup-cta.js.es6 b/app/assets/javascripts/discourse/components/signup-cta.js.es6
index d7630e26817..92df1a672ff 100644
--- a/app/assets/javascripts/discourse/components/signup-cta.js.es6
+++ b/app/assets/javascripts/discourse/components/signup-cta.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 export default Component.extend({
   action: "showCreateAccount",
@@ -10,7 +11,7 @@ export default Component.extend({
     hideForSession() {
       this.session.set("hideSignupCta", true);
       this.keyValueStore.setItem("anon-cta-hidden", new Date().getTime());
-      Ember.run.later(
+      later(
         () => this.session.set("showSignupCta", false),
         20 * 1000
       );
diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6
index 41c71a48e52..b7b82071c1a 100644
--- a/app/assets/javascripts/discourse/components/site-header.js.es6
+++ b/app/assets/javascripts/discourse/components/site-header.js.es6
@@ -1,3 +1,6 @@
+import { cancel } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import MountWidget from "discourse/components/mount-widget";
 import { observes } from "ember-addons/ember-computed-decorators";
 import Docking from "discourse/mixins/docking";
@@ -38,7 +41,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
   _animateClosing($panel, menuOrigin, windowWidth) {
     $panel.css(menuOrigin, -windowWidth);
     this._animate = true;
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this.eventDispatched("dom:clean", "header");
       this._panMenuOffset = 0;
     });
@@ -222,7 +225,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
     this.appEvents.off("header:hide-topic", this, "setTopic");
     this.appEvents.off("dom:clean", this, "_cleanDom");
 
-    Ember.run.cancel(this._scheduledRemoveAnimate);
+    cancel(this._scheduledRemoveAnimate);
     window.cancelAnimationFrame(this._scheduledMovingAnimation);
   },
 
@@ -346,7 +349,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, PanEvents, {
       if (this._animate) {
         $panel.addClass("animate");
         $headerCloak.addClass("animate");
-        this._scheduledRemoveAnimate = Ember.run.later(() => {
+        this._scheduledRemoveAnimate = later(() => {
           $panel.removeClass("animate");
           $headerCloak.removeClass("animate");
         }, 200);
diff --git a/app/assets/javascripts/discourse/components/text-overflow.js.es6 b/app/assets/javascripts/discourse/components/text-overflow.js.es6
index 37a4cb29598..d5bc066fb02 100644
--- a/app/assets/javascripts/discourse/components/text-overflow.js.es6
+++ b/app/assets/javascripts/discourse/components/text-overflow.js.es6
@@ -1,8 +1,9 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 export default Component.extend({
   didInsertElement() {
     this._super(...arguments);
-    Ember.run.next(null, () => {
+    next(null, () => {
       const $this = $(this.element);
 
       if ($this) {
diff --git a/app/assets/javascripts/discourse/components/time-input.js.es6 b/app/assets/javascripts/discourse/components/time-input.js.es6
index c9c65e1034a..6ec44fb3067 100644
--- a/app/assets/javascripts/discourse/components/time-input.js.es6
+++ b/app/assets/javascripts/discourse/components/time-input.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import { isNumeric } from "discourse/lib/utilities";
 
@@ -36,7 +37,7 @@ export default Component.extend({
           this._processMinutesChange(value);
         }
 
-        Ember.run.schedule("afterRender", () => (event.target.value = value));
+        schedule("afterRender", () => (event.target.value = value));
       }
     },
 
diff --git a/app/assets/javascripts/discourse/components/topic-entrance.js.es6 b/app/assets/javascripts/discourse/components/topic-entrance.js.es6
index f55197d8044..ead6913e821 100644
--- a/app/assets/javascripts/discourse/components/topic-entrance.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-entrance.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import DiscourseURL from "discourse/lib/url";
 import CleansUp from "discourse/mixins/cleans-up";
@@ -77,7 +78,7 @@ export default Component.extend(CleansUp, {
 
     this.setProperties({ topic: data.topic, visible: true });
 
-    Ember.run.scheduleOnce("afterRender", this, this._setCSS);
+    scheduleOnce("afterRender", this, this._setCSS);
 
     $("html")
       .off("mousedown.topic-entrance")
diff --git a/app/assets/javascripts/discourse/components/topic-list.js.es6 b/app/assets/javascripts/discourse/components/topic-list.js.es6
index c099c8b7906..44f4a39a698 100644
--- a/app/assets/javascripts/discourse/components/topic-list.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -69,7 +70,7 @@ export default Component.extend(LoadMore, {
 
     let scrollTo = this.session.get("topicListScrollPosition");
     if (scrollTo && scrollTo >= 0) {
-      Ember.run.schedule("afterRender", () =>
+      schedule("afterRender", () =>
         $(window).scrollTop(scrollTo + 1)
       );
     }
diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
index 4a67114f1a7..b2115e755d6 100644
--- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6
@@ -1,4 +1,6 @@
 import EmberObject from "@ember/object";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import { observes } from "ember-addons/ember-computed-decorators";
 import showModal from "discourse/lib/show-modal";
@@ -52,7 +54,7 @@ export default Component.extend(PanEvents, {
   },
 
   _checkSize() {
-    Ember.run.scheduleOnce("afterRender", this, this._performCheckSize);
+    scheduleOnce("afterRender", this, this._performCheckSize);
   },
 
   // we need to store this so topic progress has something to init with
@@ -88,7 +90,7 @@ export default Component.extend(PanEvents, {
   composerOpened() {
     this.set("composerOpen", true);
     // we need to do the check after animation is done
-    Ember.run.later(() => this._checkSize(), 500);
+    later(() => this._checkSize(), 500);
   },
 
   composerClosed() {
@@ -99,7 +101,7 @@ export default Component.extend(PanEvents, {
   _collapseFullscreen() {
     if (this.get("info.topicProgressExpanded")) {
       $(".timeline-fullscreen").removeClass("show");
-      Ember.run.later(() => {
+      later(() => {
         if (!this.element || this.isDestroying || this.isDestroyed) {
           return;
         }
@@ -137,7 +139,7 @@ export default Component.extend(PanEvents, {
     } else if (offset <= 0) {
       $timelineContainer.css("bottom", "");
     } else {
-      Ember.run.later(() => this._handlePanDone(offset, event), 20);
+      later(() => this._handlePanDone(offset, event), 20);
     }
   },
 
diff --git a/app/assets/javascripts/discourse/components/topic-progress.js.es6 b/app/assets/javascripts/discourse/components/topic-progress.js.es6
index 61db49b425c..9e3388ca9bf 100644
--- a/app/assets/javascripts/discourse/components/topic-progress.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-progress.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -73,7 +74,7 @@ export default Component.extend({
 
   @observes("postStream.stream.[]")
   _updateBar() {
-    Ember.run.scheduleOnce("afterRender", this, this._updateProgressBar);
+    scheduleOnce("afterRender", this, this._updateProgressBar);
   },
 
   _topicScrolled(event) {
@@ -100,16 +101,16 @@ export default Component.extend({
 
     const prevEvent = this.prevEvent;
     if (prevEvent) {
-      Ember.run.scheduleOnce(
+      scheduleOnce(
         "afterRender",
         this,
         this._topicScrolled,
         prevEvent
       );
     } else {
-      Ember.run.scheduleOnce("afterRender", this, this._updateProgressBar);
+      scheduleOnce("afterRender", this, this._updateProgressBar);
     }
-    Ember.run.scheduleOnce("afterRender", this, this._dock);
+    scheduleOnce("afterRender", this, this._dock);
   },
 
   willDestroyElement() {
diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
index 1d9ec49b787..9aa65410652 100644
--- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import MountWidget from "discourse/components/mount-widget";
 import Docking from "discourse/mixins/docking";
 import { observes } from "ember-addons/ember-computed-decorators";
@@ -94,7 +95,7 @@ export default MountWidget.extend(Docking, {
     this._super(...arguments);
 
     if (this.fullscreen && !this.addShowClass) {
-      Ember.run.next(() => {
+      next(() => {
         this.set("addShowClass", true);
         this.queueRerender();
       });
diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
index c7e2ed244a1..2210a3b47ce 100644
--- a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6
@@ -1,3 +1,5 @@
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import Component from "@ember/component";
 import { iconHTML } from "discourse-common/lib/icon-library";
 import { bufferedRender } from "discourse-common/lib/buffered-render";
@@ -85,7 +87,7 @@ export default Component.extend(
 
         // TODO Sam: concerned this can cause a heavy rerender loop
         if (!Ember.testing) {
-          this._delayedRerender = Ember.run.later(
+          this._delayedRerender = later(
             this,
             this.rerender,
             rerenderDelay
@@ -110,7 +112,7 @@ export default Component.extend(
       $(this.element).off("click.topic-timer-remove", this.removeTopicTimer);
 
       if (this._delayedRerender) {
-        Ember.run.cancel(this._delayedRerender);
+        cancel(this._delayedRerender);
       }
     },
 
diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6
index cbb9b779bc5..16c7e1fb79c 100644
--- a/app/assets/javascripts/discourse/components/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stream.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 import LoadMore from "discourse/mixins/load-more";
 import ClickTrack from "discourse/lib/click-track";
@@ -23,7 +24,7 @@ export default Component.extend(LoadMore, {
   classNames: ["user-stream"],
 
   _scrollTopOnModelChange: function() {
-    Ember.run.schedule("afterRender", () => $(document).scrollTop(0));
+    schedule("afterRender", () => $(document).scrollTop(0));
   }.observes("stream.user.id"),
 
   _inserted: Ember.on("didInsertElement", function() {
diff --git a/app/assets/javascripts/discourse/controllers/auth-token.js.es6 b/app/assets/javascripts/discourse/controllers/auth-token.js.es6
index 0a02adf2231..a24f6a53b18 100644
--- a/app/assets/javascripts/discourse/controllers/auth-token.js.es6
+++ b/app/assets/javascripts/discourse/controllers/auth-token.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
 import { ajax } from "discourse/lib/ajax";
@@ -24,7 +25,7 @@ export default Controller.extend(ModalFunctionality, {
     highlightSecure() {
       this.send("closeModal");
 
-      Ember.run.next(() => {
+      next(() => {
         const $prefPasswordDiv = $(".pref-password");
 
         $prefPasswordDiv.addClass("highlighted");
diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6
index 28acef583ea..6496ccb0be8 100644
--- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6
+++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
@@ -53,7 +54,7 @@ export default Controller.extend(ModalFunctionality, {
           if (this.get("topicController.multiSelect")) {
             this.topicController.send("toggleMultiSelect");
           }
-          Ember.run.next(() =>
+          next(() =>
             DiscourseURL.routeTo(this.get("topicController.model.url"))
           );
         },
diff --git a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
index 7744b066e1d..0251ab88834 100644
--- a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
+++ b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
@@ -47,7 +48,7 @@ export default Controller.extend(ModalFunctionality, {
         .then(() => {
           this.send("closeModal");
           this.setProperties({ date: "", time: "", saving: false });
-          Ember.run.next(() => DiscourseURL.routeTo(topic.url));
+          next(() => DiscourseURL.routeTo(topic.url));
         })
         .catch(() =>
           this.flash(I18n.t("topic.change_timestamp.error"), "alert-error")
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index 01867e7c6a9..a4ecfb84761 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 import { inject as service } from "@ember/service";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
@@ -1046,7 +1047,7 @@ export default Controller.extend({
 
   @observes("model.reply", "model.title")
   _shouldSaveDraft() {
-    Ember.run.debounce(this, this._saveDraft, 2000);
+    debounce(this, this._saveDraft, 2000);
   },
 
   @computed("model.categoryId", "lastValidatedAt")
diff --git a/app/assets/javascripts/discourse/controllers/exception.js.es6 b/app/assets/javascripts/discourse/controllers/exception.js.es6
index 7b8a1bec8f0..2e3f44ae749 100644
--- a/app/assets/javascripts/discourse/controllers/exception.js.es6
+++ b/app/assets/javascripts/discourse/controllers/exception.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Controller from "@ember/controller";
 import {
   on,
@@ -113,7 +114,7 @@ export default Controller.extend({
     tryLoading() {
       this.set("loading", true);
 
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         this.lastTransition.retry();
         this.set("loading", false);
       });
diff --git a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6 b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
index e1d714be399..63b724f89cc 100644
--- a/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
+++ b/app/assets/javascripts/discourse/controllers/insert-hyperlink.js.es6
@@ -1,3 +1,6 @@
+import { debounce } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
 import { searchForTerm } from "discourse/lib/search";
@@ -15,7 +18,7 @@ export default Controller.extend(ModalFunctionality, {
       selectedRow: -1
     });
 
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       const element = document.querySelector(".insert-link");
 
       element.addEventListener("keydown", e => this.keyDown(e));
@@ -135,7 +138,7 @@ export default Controller.extend(ModalFunctionality, {
       .closest(".modal-inner-container")
       .removeEventListener("mousedown", this.mouseDown);
 
-    Ember.run.cancel(this._debounced);
+    cancel(this._debounced);
   },
 
   actions: {
@@ -174,7 +177,7 @@ export default Controller.extend(ModalFunctionality, {
       }
     },
     search() {
-      this._debounced = Ember.run.debounce(this, this.triggerSearch, 400);
+      this._debounced = debounce(this, this.triggerSearch, 400);
     }
   }
 });
diff --git a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6 b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
index af6fd216f57..a0d4bd59503 100644
--- a/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
+++ b/app/assets/javascripts/discourse/controllers/jump-to-post.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
 
@@ -10,7 +11,7 @@ export default Controller.extend(ModalFunctionality, {
   ),
 
   onShow() {
-    Ember.run.next(() => $("#post-jump").focus());
+    next(() => $("#post-jump").focus());
   },
 
   actions: {
diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6
index ca94d979c00..25011468057 100644
--- a/app/assets/javascripts/discourse/controllers/login.js.es6
+++ b/app/assets/javascripts/discourse/controllers/login.js.es6
@@ -1,4 +1,6 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import { ajax } from "discourse/lib/ajax";
@@ -150,7 +152,7 @@ export default Controller.extend(ModalFunctionality, {
 
               // only need to focus the 2FA input for TOTP
               if (!this.showSecurityKey) {
-                Ember.run.scheduleOnce("afterRender", () =>
+                scheduleOnce("afterRender", () =>
                   document
                     .getElementById("second-factor")
                     .querySelector("input")
@@ -322,7 +324,7 @@ export default Controller.extend(ModalFunctionality, {
     const loginError = (errorMsg, className, callback) => {
       showModal("login");
 
-      Ember.run.next(() => {
+      next(() => {
         if (callback) callback();
         this.flash(errorMsg, className || "success");
       });
diff --git a/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6 b/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6
index 8a89fc8e575..7c8c84648c0 100644
--- a/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/move-to-topic.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import ModalFunctionality from "discourse/mixins/modal-functionality";
@@ -92,7 +93,7 @@ export default Controller.extend(ModalFunctionality, {
       );
     } else if (!this.canSplitTopic) {
       this.set("selection", "existing_topic");
-      Ember.run.next(() => $("#choose-topic-title").focus());
+      next(() => $("#choose-topic-title").focus());
     }
   },
 
diff --git a/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6 b/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6
index ddddfbdb30c..6554a77cd1f 100644
--- a/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6
+++ b/app/assets/javascripts/discourse/controllers/second-factor-backup-edit.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import Controller from "@ember/controller";
 import { default as computed } from "ember-addons/ember-computed-decorators";
 import { SECOND_FACTOR_METHODS } from "discourse/models/user";
@@ -102,7 +103,7 @@ export default Controller.extend(ModalFunctionality, {
   },
 
   _hideCopyMessage() {
-    Ember.run.later(
+    later(
       () => this.setProperties({ successMessage: null, errorMessage: null }),
       2000
     );
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index d31fe002922..06ac89f3ad2 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -1,4 +1,6 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import { inject } from "@ember/controller";
 import Controller from "@ember/controller";
 import { bufferedProperty } from "discourse/mixins/buffered-content";
@@ -133,7 +135,7 @@ export default Controller.extend(bufferedProperty("model"), {
       return;
     }
 
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       this.send("showHistory", post, revision);
     });
   },
@@ -1426,7 +1428,7 @@ export default Controller.extend(bufferedProperty("model"), {
         // automatically unpin topics when the user reaches the bottom
         const max = _.max(postNumbers);
         if (topic.get("pinned") && max >= topic.get("highest_post_number")) {
-          Ember.run.next(() => topic.clearPin());
+          next(() => topic.clearPin());
         }
       }
     }
diff --git a/app/assets/javascripts/discourse/initializers/asset-version.js.es6 b/app/assets/javascripts/discourse/initializers/asset-version.js.es6
index 21faedd6502..f685d1b091a 100644
--- a/app/assets/javascripts/discourse/initializers/asset-version.js.es6
+++ b/app/assets/javascripts/discourse/initializers/asset-version.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 //  Subscribe to "asset-version" change events via the Message Bus
 export default {
   name: "asset-version",
@@ -16,7 +17,7 @@ export default {
       if (!timeoutIsSet && Discourse.get("requiresRefresh")) {
         // Since we can do this transparently for people browsing the forum
         //  hold back the message 24 hours.
-        Ember.run.later(() => {
+        later(() => {
           bootbox.confirm(I18n.t("assets_changed_confirm"), function(result) {
             if (result) {
               document.location.reload();
diff --git a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6 b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
index 83e0183ff3b..31101d9010c 100644
--- a/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
+++ b/app/assets/javascripts/discourse/initializers/auth-complete.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 export default {
   name: "auth-complete",
   after: "inject-objects",
@@ -13,7 +14,7 @@ export default {
     if (lastAuthResult) {
       const router = container.lookup("router:main");
       router.one("didTransition", () => {
-        Ember.run.next(() =>
+        next(() =>
           Discourse.authenticationComplete(JSON.parse(lastAuthResult))
         );
       });
diff --git a/app/assets/javascripts/discourse/initializers/mobile.js.es6 b/app/assets/javascripts/discourse/initializers/mobile.js.es6
index a536398e94e..b0708fca852 100644
--- a/app/assets/javascripts/discourse/initializers/mobile.js.es6
+++ b/app/assets/javascripts/discourse/initializers/mobile.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import Mobile from "discourse/lib/mobile";
 import { setResolverOption } from "discourse-common/resolver";
 import { isAppWebview, postRNWebviewMessage } from "discourse/lib/utilities";
@@ -17,7 +18,7 @@ export default {
     setResolverOption("mobileView", Mobile.mobileView);
 
     if (isAppWebview()) {
-      Ember.run.later(() => {
+      later(() => {
         postRNWebviewMessage(
           "headerBg",
           $(".d-header").css("background-color")
diff --git a/app/assets/javascripts/discourse/lib/ajax.js.es6 b/app/assets/javascripts/discourse/lib/ajax.js.es6
index cafa6e6f709..e2bb45ef085 100644
--- a/app/assets/javascripts/discourse/lib/ajax.js.es6
+++ b/app/assets/javascripts/discourse/lib/ajax.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import pageVisible from "discourse/lib/page-visible";
 import logout from "discourse/lib/logout";
 
@@ -96,7 +97,7 @@ export function ajax() {
       handleRedirect(data);
       handleLogoff(xhr);
 
-      Ember.run(() => {
+      run(() => {
         Discourse.Site.currentProp(
           "isReadOnly",
           !!xhr.getResponseHeader("Discourse-Readonly")
@@ -107,7 +108,7 @@ export function ajax() {
         data = { result: data, xhr: xhr };
       }
 
-      Ember.run(null, resolve, data);
+      run(null, resolve, data);
     };
 
     args.error = (xhr, textStatus, errorThrown) => {
@@ -128,7 +129,7 @@ export function ajax() {
       xhr.jqTextStatus = textStatus;
       xhr.requestedUrl = url;
 
-      Ember.run(null, reject, {
+      run(null, reject, {
         jqXHR: xhr,
         textStatus: textStatus,
         errorThrown: errorThrown
diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6
index 18fc9a98e2b..0bcb3331c89 100644
--- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6
+++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6
@@ -1,3 +1,5 @@
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
 /**
   This is a jQuery plugin to support autocompleting values in our text fields.
 
@@ -43,7 +45,7 @@ export default function(options) {
   if (this.length === 0) return;
 
   if (options === "destroy" || options.updateData) {
-    Ember.run.cancel(inputTimeout);
+    cancel(inputTimeout);
 
     $(this)
       .off("keyup.autocomplete")
@@ -402,7 +404,7 @@ export default function(options) {
   $(this).on("click.autocomplete", () => closeAutocomplete());
 
   $(this).on("paste.autocomplete", () => {
-    Ember.run.later(() => me.trigger("keydown"), 50);
+    later(() => me.trigger("keydown"), 50);
   });
 
   function checkTriggerRule(opts) {
@@ -455,8 +457,8 @@ export default function(options) {
     if (options.allowAny) {
       // saves us wiring up a change event as well
 
-      Ember.run.cancel(inputTimeout);
-      inputTimeout = Ember.run.later(function() {
+      cancel(inputTimeout);
+      inputTimeout = later(function() {
         if (inputSelectedItems.length === 0) {
           inputSelectedItems.push("");
         }
diff --git a/app/assets/javascripts/discourse/lib/clean-dom.js.es6 b/app/assets/javascripts/discourse/lib/clean-dom.js.es6
index 8d83f4d7f80..82cdb2d5371 100644
--- a/app/assets/javascripts/discourse/lib/clean-dom.js.es6
+++ b/app/assets/javascripts/discourse/lib/clean-dom.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 function _clean() {
   if (window.MiniProfiler) {
     window.MiniProfiler.pageTransition();
@@ -35,5 +36,5 @@ function _clean() {
 }
 
 export function cleanDOM() {
-  Ember.run.scheduleOnce("afterRender", _clean);
+  scheduleOnce("afterRender", _clean);
 }
diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6
index 42a65623a6b..6e27ebbbe25 100644
--- a/app/assets/javascripts/discourse/lib/click-track.js.es6
+++ b/app/assets/javascripts/discourse/lib/click-track.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 import DiscourseURL from "discourse/lib/url";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
@@ -134,7 +135,7 @@ export default {
           $link.attr("href", null);
           $link.data("auto-route", true);
 
-          Ember.run.later(() => {
+          later(() => {
             $link.removeClass("no-href");
             $link.attr("href", $link.data("href"));
             $link.data("href", null);
diff --git a/app/assets/javascripts/discourse/lib/debounce.js.es6 b/app/assets/javascripts/discourse/lib/debounce.js.es6
index 6ab2eb1e584..f41b71f0d8e 100644
--- a/app/assets/javascripts/discourse/lib/debounce.js.es6
+++ b/app/assets/javascripts/discourse/lib/debounce.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 /**
   Debounce a Javascript function. This means if it's called many times in a time limit it
   should only be executed once (at the end of the limit counted from the last call made).
@@ -13,6 +14,6 @@ export default function(func, wait) {
     self = this;
     args = arguments;
 
-    Ember.run.debounce(null, later, wait);
+    debounce(null, later, wait);
   };
 }
diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
index 41b86bc30e2..330772014d1 100644
--- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
+++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import DiscourseURL from "discourse/lib/url";
 import KeyValueStore from "discourse/lib/key-value-store";
 import { formatUsername } from "discourse/lib/utilities";
@@ -79,7 +80,7 @@ function confirmNotification() {
   const clickEventHandler = () => notification.close();
 
   notification.addEventListener("click", clickEventHandler);
-  Ember.run.later(() => {
+  later(() => {
     notification.close();
     notification.removeEventListener("click", clickEventHandler);
   }, 10 * 1000);
@@ -177,7 +178,7 @@ function onNotification(data) {
     }
 
     notification.addEventListener("click", clickEventHandler);
-    Ember.run.later(() => {
+    later(() => {
       notification.close();
       notification.removeEventListener("click", clickEventHandler);
     }, 10 * 1000);
diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
index aa0b16efbb3..991644737af 100644
--- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
+++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
@@ -1,3 +1,5 @@
+import { run } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import DiscourseURL from "discourse/lib/url";
 import Composer from "discourse/models/composer";
 import { minimumOffset } from "discourse/lib/offset-calculator";
@@ -140,7 +142,7 @@ export default {
   quoteReply() {
     this.sendToSelectedPost("replyToPost");
     // lazy but should work for now
-    Ember.run.later(() => $(".d-editor .quote").click(), 500);
+    later(() => $(".d-editor .quote").click(), 500);
 
     return false;
   },
@@ -210,7 +212,7 @@ export default {
   },
 
   showPageSearch(event) {
-    Ember.run(() => {
+    run(() => {
       this.appEvents.trigger("header:keyboard-trigger", {
         type: "page-search",
         event
@@ -219,7 +221,7 @@ export default {
   },
 
   printTopic(event) {
-    Ember.run(() => {
+    run(() => {
       if ($(".container.posts").length) {
         event.preventDefault(); // We need to stop printing the current page in Firefox
         this.container.lookup("controller:topic").print();
diff --git a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6
index d43b0f4a838..7179f234f19 100644
--- a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 import { replaceSpan } from "discourse/lib/category-hashtags";
 
@@ -7,7 +8,7 @@ const testedKey = "tested";
 const testedClass = `hashtag-${testedKey}`;
 
 function updateFound($hashtags, categorySlugs) {
-  Ember.run.schedule("afterRender", () => {
+  schedule("afterRender", () => {
     $hashtags.each((index, hashtag) => {
       const categorySlug = categorySlugs[index];
       const link = validCategoryHashtags[categorySlug];
diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
index 07f37388e4b..68d0b8b5d6f 100644
--- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 import { userPath } from "discourse/lib/url";
 import { formatUsername } from "discourse/lib/utilities";
@@ -39,7 +40,7 @@ const checked = {};
 const cannotSee = [];
 
 function updateFound($mentions, usernames) {
-  Ember.run.scheduleOnce("afterRender", function() {
+  scheduleOnce("afterRender", function() {
     $mentions.each((i, e) => {
       const $e = $(e);
       const username = usernames[i];
diff --git a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
index a0b2c95c467..5aa3f6d6120 100644
--- a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 import { replaceSpan } from "discourse/lib/category-hashtags";
 import { TAG_HASHTAG_POSTFIX } from "discourse/lib/tag-hashtags";
@@ -7,7 +8,7 @@ const checkedTagHashtags = [];
 const testedClass = "tag-hashtag-tested";
 
 function updateFound($hashtags, tagValues) {
-  Ember.run.schedule("afterRender", () => {
+  schedule("afterRender", () => {
     $hashtags.each((index, hashtag) => {
       const tagValue = tagValues[index];
       const link = validTagHashtags[tagValue];
diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6
index 34b3f2897b7..c1f11ec0f49 100644
--- a/app/assets/javascripts/discourse/lib/load-script.js.es6
+++ b/app/assets/javascripts/discourse/lib/load-script.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 const _loaded = {};
 const _loading = {};
@@ -22,7 +23,7 @@ function loadWithTag(path, cb) {
     ) {
       s = s.onload = s.onreadystatechange = null;
       if (!abort) {
-        Ember.run(null, cb);
+        run(null, cb);
       }
     }
   };
diff --git a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
index d7577ed5ae8..4acbdc4ea36 100644
--- a/app/assets/javascripts/discourse/lib/page-tracker.js.es6
+++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 let _started = false;
 let cache = {};
 let transitionCount = 0;
@@ -31,7 +32,7 @@ export function startPageTracking(router, appEvents) {
 
     // Refreshing the title is debounced, so we need to trigger this in the
     // next runloop to have the correct title.
-    Ember.run.next(() => {
+    next(() => {
       let title = Discourse.get("_docTitle");
 
       appEvents.trigger("page:changed", {
diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
index e613ba16e36..5d5a10b0dc5 100644
--- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
+++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import debounce from "discourse/lib/debounce";
 import {
   safariHacksDisabled,
@@ -94,7 +95,7 @@ function positioningWorkaround($fixedElement) {
 
       if (!iOSWithVisualViewport()) {
         fixedElement.style.height = oldHeight;
-        Ember.run.later(
+        later(
           () => $(fixedElement).removeClass("no-transition"),
           500
         );
diff --git a/app/assets/javascripts/discourse/lib/screen-track.js.es6 b/app/assets/javascripts/discourse/lib/screen-track.js.es6
index a36b15cb219..314e1fac0a2 100644
--- a/app/assets/javascripts/discourse/lib/screen-track.js.es6
+++ b/app/assets/javascripts/discourse/lib/screen-track.js.es6
@@ -1,3 +1,4 @@
+import { bind } from "@ember/runloop";
 import { ajax } from "discourse/lib/ajax";
 
 // We use this class to track how long posts in a topic are on the screen.
@@ -26,7 +27,7 @@ export default class {
     // Create an interval timer if we don't have one.
     if (!this._interval) {
       this._interval = setInterval(() => this.tick(), 1000);
-      this._boundScrolled = Ember.run.bind(this, this.scrolled);
+      this._boundScrolled = bind(this, this.scrolled);
       $(window).on("scroll.screentrack", this._boundScrolled);
     }
 
diff --git a/app/assets/javascripts/discourse/lib/throttle.js.es6 b/app/assets/javascripts/discourse/lib/throttle.js.es6
index 2e8673203ba..05daa36c5a6 100644
--- a/app/assets/javascripts/discourse/lib/throttle.js.es6
+++ b/app/assets/javascripts/discourse/lib/throttle.js.es6
@@ -1,3 +1,4 @@
+import { throttle } from "@ember/runloop";
 /**
   Throttle a Javascript function. This means if it's called many times in a time limit it
   should only be executed one time at most during this time limit
@@ -13,6 +14,6 @@ export default function(func, spacing, immediate) {
     self = this;
     args = arguments;
 
-    Ember.run.throttle(null, later, spacing, immediate);
+    throttle(null, later, spacing, immediate);
   };
 }
diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6
index 085619f9e93..0dbb7aa3ce2 100644
--- a/app/assets/javascripts/discourse/lib/url.js.es6
+++ b/app/assets/javascripts/discourse/lib/url.js.es6
@@ -1,4 +1,6 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import offsetCalculator from "discourse/lib/offset-calculator";
 import LockOn from "discourse/lib/lock-on";
 import { defaultHomepage } from "discourse/lib/utilities";
@@ -66,7 +68,7 @@ export function jumpToElement(elementId) {
 
   const selector = `#${elementId}, a[name=${elementId}]`;
   _jumpScheduled = true;
-  Ember.run.schedule("afterRender", function() {
+  schedule("afterRender", function() {
     const lockon = new LockOn(selector, {
       finished() {
         _jumpScheduled = false;
@@ -90,7 +92,7 @@ const DiscourseURL = EmberObject.extend({
 
     _transitioning = postNumber > 1;
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       let elementId;
       let holder;
 
@@ -156,7 +158,7 @@ const DiscourseURL = EmberObject.extend({
       // Always use replaceState in the next runloop to prevent weird routes changing
       // while URLs are loading. For example, while a topic loads it sets `currentPost`
       // which triggers a replaceState even though the topic hasn't fully loaded yet!
-      Ember.run.next(() => {
+      next(() => {
         const location = DiscourseURL.get("router.location");
         if (location && location.replaceURL) {
           location.replaceURL(path);
@@ -250,7 +252,7 @@ const DiscourseURL = EmberObject.extend({
     path = rewritePath(path);
 
     if (typeof opts.afterRouteComplete === "function") {
-      Ember.run.schedule("afterRender", opts.afterRouteComplete);
+      schedule("afterRender", opts.afterRouteComplete);
     }
 
     if (this.navigatedToPost(oldPath, path, opts)) {
diff --git a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6 b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
index 212278f2e0f..69bafea2fb9 100644
--- a/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
+++ b/app/assets/javascripts/discourse/mixins/card-contents-base.js.es6
@@ -1,3 +1,6 @@
+import { throttle } from "@ember/runloop";
+import { next } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import { wantsNewWindow } from "discourse/lib/intercept-click";
 import afterTransition from "discourse/lib/after-transition";
 import DiscourseURL from "discourse/lib/url";
@@ -142,7 +145,7 @@ export default Ember.Mixin.create({
   _bindMobileScroll() {
     const mobileScrollEvent = this.mobileScrollEvent;
     const onScroll = () => {
-      Ember.run.throttle(this, this._close, 1000);
+      throttle(this, this._close, 1000);
     };
 
     $(window).on(mobileScrollEvent, onScroll);
@@ -171,7 +174,7 @@ export default Ember.Mixin.create({
 
     let verticalAdjustments = 0;
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       if (target) {
         if (!this.site.mobileView) {
           let position = target.offset();
@@ -245,7 +248,7 @@ export default Ember.Mixin.create({
         // note: we DO NOT use afterRender here cause _positionCard may
         // run afterwards, if we allowed this to happen the usercard
         // may be offscreen and we may scroll all the way to it on focus
-        Ember.run.next(null, () => {
+        next(null, () => {
           const firstLink = this.element.querySelector("a");
           firstLink && firstLink.focus();
         });
diff --git a/app/assets/javascripts/discourse/mixins/docking.js.es6 b/app/assets/javascripts/discourse/mixins/docking.js.es6
index 800898ae70a..18a5ae5bf64 100644
--- a/app/assets/javascripts/discourse/mixins/docking.js.es6
+++ b/app/assets/javascripts/discourse/mixins/docking.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 const helper = {
   offset() {
     const mainOffset = $("#main").offset();
@@ -12,7 +13,7 @@ export default Ember.Mixin.create({
   init() {
     this._super(...arguments);
     this.queueDockCheck = () => {
-      Ember.run.debounce(this, this.safeDockCheck, 5);
+      debounce(this, this.safeDockCheck, 5);
     };
   },
 
diff --git a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6 b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
index 0f27500a094..edd93096f02 100644
--- a/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
+++ b/app/assets/javascripts/discourse/mixins/mobile-scroll-direction.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 // Small buffer so that very tiny scrolls don't trigger mobile header switch
 const MOBILE_SCROLL_TOLERANCE = 5;
 
@@ -42,7 +43,7 @@ export default Ember.Mixin.create({
     // If the user reaches the very bottom of the topic, we only want to reset
     // this scroll direction after a second scrolldown. This is a nicer event
     // similar to what Safari and Chrome do.
-    Ember.run.debounce(() => {
+    debounce(() => {
       this._bottomHit = 1;
     }, 1000);
 
diff --git a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
index 630227b8fa5..f666e9be5a9 100644
--- a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
+++ b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import DiscourseURL from "discourse/lib/url";
 import { deprecated } from "discourse/mixins/scroll-top";
 
@@ -14,7 +15,7 @@ function scrollTop() {
   if (DiscourseURL.isJumpScheduled()) {
     return;
   }
-  Ember.run.scheduleOnce("afterRender", context, context._scrollTop);
+  scheduleOnce("afterRender", context, context._scrollTop);
 }
 
 export default Ember.Mixin.create({
diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 b/app/assets/javascripts/discourse/mixins/scrolling.js.es6
index 690bf1168fd..f51474c0b20 100644
--- a/app/assets/javascripts/discourse/mixins/scrolling.js.es6
+++ b/app/assets/javascripts/discourse/mixins/scrolling.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import debounce from "discourse/lib/debounce";
 
 /**
@@ -37,7 +38,7 @@ const Scrolling = Ember.Mixin.create({
       if (router.activeTransition) {
         return;
       }
-      return Ember.run.scheduleOnce("afterRender", this, "scrolled");
+      return scheduleOnce("afterRender", this, "scrolled");
     };
 
     if (opts.debounce) {
diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 4d851e6e3a9..3a265ab342b 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -1,4 +1,7 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import RestModel from "discourse/models/rest";
 import Topic from "discourse/models/topic";
 import { throwAjaxError } from "discourse/lib/ajax-error";
@@ -994,7 +997,7 @@ const Composer = RestModel.extend({
               post.set("reply_count", post.reply_count - 1);
             }
           }
-          Ember.run.next(() => composer.set("composeState", OPEN));
+          next(() => composer.set("composeState", OPEN));
         })
       );
   },
@@ -1045,7 +1048,7 @@ const Composer = RestModel.extend({
     });
 
     if (this._clearingStatus) {
-      Ember.run.cancel(this._clearingStatus);
+      cancel(this._clearingStatus);
       this._clearingStatus = null;
     }
 
@@ -1085,7 +1088,7 @@ const Composer = RestModel.extend({
     const draftStatus = this.draftStatus;
 
     if (draftStatus && !this._clearingStatus) {
-      this._clearingStatus = Ember.run.later(
+      this._clearingStatus = later(
         this,
         () => {
           this.setProperties({ draftStatus: null, draftConflictUser: null });
diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6
index 4130d7ffe63..cb1e5f0406c 100644
--- a/app/assets/javascripts/discourse/routes/application.js.es6
+++ b/app/assets/javascripts/discourse/routes/application.js.es6
@@ -1,3 +1,5 @@
+import { once } from "@ember/runloop";
+import { next } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import { ajax } from "discourse/lib/ajax";
 import { setting } from "discourse/lib/computed";
@@ -56,7 +58,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
     // Ember doesn't provider a router `willTransition` event so let's make one
     willTransition() {
       var router = getOwner(this).lookup("router:main");
-      Ember.run.once(router, router.trigger, "willTransition");
+      once(router, router.trigger, "willTransition");
       return this._super(...arguments);
     },
 
@@ -226,7 +228,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
 
   activate() {
     this._super(...arguments);
-    Ember.run.next(function() {
+    next(function() {
       // Support for callbacks once the application has activated
       ApplicationRoute.trigger("activate");
     });
diff --git a/app/assets/javascripts/discourse/routes/associate-account.js.es6 b/app/assets/javascripts/discourse/routes/associate-account.js.es6
index 7cbfee67548..a182e85c88f 100644
--- a/app/assets/javascripts/discourse/routes/associate-account.js.es6
+++ b/app/assets/javascripts/discourse/routes/associate-account.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import { ajax } from "discourse/lib/ajax";
 import showModal from "discourse/lib/show-modal";
@@ -7,7 +8,7 @@ export default DiscourseRoute.extend({
   beforeModel() {
     const params = this.paramsFor("associate-account");
     this.replaceWith(`preferences.account`, this.currentUser).then(() =>
-      Ember.run.next(() =>
+      next(() =>
         ajax(`/associate/${encodeURIComponent(params.token)}.json`)
           .then(model => showModal("associate-account-confirm", { model }))
           .catch(popupAjaxError)
diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6
index fb828bddd1d..2329168593e 100644
--- a/app/assets/javascripts/discourse/routes/discourse.js.es6
+++ b/app/assets/javascripts/discourse/routes/discourse.js.es6
@@ -1,3 +1,4 @@
+import { once } from "@ember/runloop";
 import Composer from "discourse/models/composer";
 import { getOwner } from "discourse-common/lib/get-owner";
 import Route from "@ember/routing/route";
@@ -55,7 +56,7 @@ const DiscourseRoute = Route.extend({
     },
 
     refreshTitle() {
-      Ember.run.once(this, this._refreshTitleOnce);
+      once(this, this._refreshTitleOnce);
     },
 
     clearTopicDraft() {
diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
index 19543ae481f..1510856c114 100644
--- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import showModal from "discourse/lib/show-modal";
 import OpenComposer from "discourse/mixins/open-composer";
@@ -134,7 +135,7 @@ const DiscoveryCategoriesRoute = DiscourseRoute.extend(OpenComposer, {
     },
 
     didTransition() {
-      Ember.run.next(() =>
+      next(() =>
         this.controllerFor("application").set("showFooter", true)
       );
       return true;
diff --git a/app/assets/javascripts/discourse/routes/forgot-password.js.es6 b/app/assets/javascripts/discourse/routes/forgot-password.js.es6
index e88fbdb0821..58309565b5d 100644
--- a/app/assets/javascripts/discourse/routes/forgot-password.js.es6
+++ b/app/assets/javascripts/discourse/routes/forgot-password.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { defaultHomepage } from "discourse/lib/utilities";
 import buildStaticRoute from "discourse/routes/build-static-route";
 
@@ -11,7 +12,7 @@ ForgotPasswordRoute.reopen({
     this.replaceWith(
       loginRequired ? "login" : `discovery.${defaultHomepage()}`
     ).then(e => {
-      Ember.run.next(() => e.send("showForgotPassword"));
+      next(() => e.send("showForgotPassword"));
     });
   }
 });
diff --git a/app/assets/javascripts/discourse/routes/login.js.es6 b/app/assets/javascripts/discourse/routes/login.js.es6
index 8465ceb6bcc..c41f884f301 100644
--- a/app/assets/javascripts/discourse/routes/login.js.es6
+++ b/app/assets/javascripts/discourse/routes/login.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import buildStaticRoute from "discourse/routes/build-static-route";
 import { defaultHomepage } from "discourse/lib/utilities";
 
@@ -7,7 +8,7 @@ LoginRoute.reopen({
   beforeModel() {
     if (!this.siteSettings.login_required) {
       this.replaceWith(`/${defaultHomepage()}`).then(e => {
-        Ember.run.next(() => e.send("showLogin"));
+        next(() => e.send("showLogin"));
       });
     }
   }
diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6
index 1410c65356f..c85001cbbf2 100644
--- a/app/assets/javascripts/discourse/routes/new-message.js.es6
+++ b/app/assets/javascripts/discourse/routes/new-message.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import User from "discourse/models/user";
 import Group from "discourse/models/group";
@@ -15,7 +16,7 @@ export default DiscourseRoute.extend({
           User.findByUsername(encodeURIComponent(params.username))
             .then(user => {
               if (user.can_send_private_message_to_user) {
-                Ember.run.next(() =>
+                next(() =>
                   e.send(
                     "createNewMessageViaParams",
                     user.username,
@@ -35,7 +36,7 @@ export default DiscourseRoute.extend({
           Group.messageable(groupName)
             .then(result => {
               if (result.messageable) {
-                Ember.run.next(() =>
+                next(() =>
                   e.send(
                     "createNewMessageViaParams",
                     groupName,
diff --git a/app/assets/javascripts/discourse/routes/new-topic.js.es6 b/app/assets/javascripts/discourse/routes/new-topic.js.es6
index 82b2b496efa..550b4b5341c 100644
--- a/app/assets/javascripts/discourse/routes/new-topic.js.es6
+++ b/app/assets/javascripts/discourse/routes/new-topic.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import Category from "discourse/models/category";
 
@@ -63,7 +64,7 @@ export default DiscourseRoute.extend({
   },
 
   _sendTransition(event, transition, categoryId) {
-    Ember.run.next(() => {
+    next(() => {
       event.send(
         "createNewTopicViaParams",
         transition.to.queryParams.title,
diff --git a/app/assets/javascripts/discourse/routes/signup.js.es6 b/app/assets/javascripts/discourse/routes/signup.js.es6
index a2753ded2bf..9a732c67d8f 100644
--- a/app/assets/javascripts/discourse/routes/signup.js.es6
+++ b/app/assets/javascripts/discourse/routes/signup.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import buildStaticRoute from "discourse/routes/build-static-route";
 
 const SignupRoute = buildStaticRoute("signup");
@@ -9,13 +10,13 @@ SignupRoute.reopen({
     if (!this.siteSettings.login_required) {
       this.replaceWith("discovery.latest").then(e => {
         if (canSignUp) {
-          Ember.run.next(() => e.send("showCreateAccount"));
+          next(() => e.send("showCreateAccount"));
         }
       });
     } else {
       this.replaceWith("login").then(e => {
         if (canSignUp) {
-          Ember.run.next(() => e.send("showCreateAccount"));
+          next(() => e.send("showCreateAccount"));
         }
       });
     }
diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
index c3d0e6cf727..6f626644a4a 100644
--- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import DiscourseURL from "discourse/lib/url";
 import Draft from "discourse/models/draft";
@@ -57,7 +58,7 @@ export default DiscourseRoute.extend({
         topicController.subscribe();
 
         // Highlight our post after the next render
-        Ember.run.scheduleOnce("afterRender", () =>
+        scheduleOnce("afterRender", () =>
           this.appEvents.trigger("post:highlight", closest)
         );
 
diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6
index e4b168ec7a9..f992c01c5b3 100644
--- a/app/assets/javascripts/discourse/routes/topic.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic.js.es6
@@ -1,3 +1,6 @@
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import DiscourseRoute from "discourse/routes/discourse";
 import DiscourseURL from "discourse/lib/url";
 import { ID_CONSTRAINT } from "discourse/models/topic";
@@ -168,9 +171,9 @@ const TopicRoute = DiscourseRoute.extend({
           postUrl += "/" + currentPost;
         }
 
-        Ember.run.cancel(scheduledReplace);
+        cancel(scheduledReplace);
         lastScrollPos = parseInt($(document).scrollTop(), 10);
-        scheduledReplace = Ember.run.later(
+        scheduledReplace = later(
           this,
           "_replaceUnlessScrolling",
           postUrl,
@@ -186,7 +189,7 @@ const TopicRoute = DiscourseRoute.extend({
 
     willTransition() {
       this._super(...arguments);
-      Ember.run.cancel(scheduledReplace);
+      cancel(scheduledReplace);
       isTransitioning = true;
       return true;
     }
@@ -201,7 +204,7 @@ const TopicRoute = DiscourseRoute.extend({
       return;
     }
     lastScrollPos = currentPos;
-    scheduledReplace = Ember.run.later(
+    scheduledReplace = later(
       this,
       "_replaceUnlessScrolling",
       url,
@@ -302,7 +305,7 @@ const TopicRoute = DiscourseRoute.extend({
     // We reset screen tracking every time a topic is entered
     this.screenTrack.start(model.get("id"), controller);
 
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       this.appEvents.trigger("header:update-topic", model);
     });
   }
diff --git a/app/assets/javascripts/discourse/widgets/component-connector.js.es6 b/app/assets/javascripts/discourse/widgets/component-connector.js.es6
index 44add9ca7ea..44eabe7c033 100644
--- a/app/assets/javascripts/discourse/widgets/component-connector.js.es6
+++ b/app/assets/javascripts/discourse/widgets/component-connector.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 export default class ComponentConnector {
   constructor(widget, componentName, opts, trackedProperties) {
     this.widget = widget;
@@ -13,7 +14,7 @@ export default class ComponentConnector {
     const elem = $elem[0];
     const { opts, widget, componentName } = this;
 
-    Ember.run.next(() => {
+    next(() => {
       const mounted = widget._findView();
 
       const view = widget.register
diff --git a/app/assets/javascripts/discourse/widgets/connector.js.es6 b/app/assets/javascripts/discourse/widgets/connector.js.es6
index b077bde0eba..d5d1c98f3d2 100644
--- a/app/assets/javascripts/discourse/widgets/connector.js.es6
+++ b/app/assets/javascripts/discourse/widgets/connector.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import deprecated from "discourse-common/lib/deprecated";
 
 export default class Connector {
@@ -11,7 +12,7 @@ export default class Connector {
     const elem = $elem[0];
 
     const { opts, widget } = this;
-    Ember.run.next(() => {
+    next(() => {
       const mounted = widget._findView();
 
       if (opts.templateName) {
diff --git a/app/assets/javascripts/discourse/widgets/glue.js.es6 b/app/assets/javascripts/discourse/widgets/glue.js.es6
index 97fcbd3fe5c..c3a6af4b802 100644
--- a/app/assets/javascripts/discourse/widgets/glue.js.es6
+++ b/app/assets/javascripts/discourse/widgets/glue.js.es6
@@ -1,3 +1,5 @@
+import { cancel } from "@ember/runloop";
+import { scheduleOnce } from "@ember/runloop";
 import { diff, patch } from "virtual-dom";
 import { queryRegistry } from "discourse/widgets/widget";
 import DirtyKeys from "discourse/lib/dirty-keys";
@@ -25,11 +27,11 @@ export default class WidgetGlue {
   }
 
   queueRerender() {
-    this._timeout = Ember.run.scheduleOnce("render", this, this.rerenderWidget);
+    this._timeout = scheduleOnce("render", this, this.rerenderWidget);
   }
 
   rerenderWidget() {
-    Ember.run.cancel(this._timeout);
+    cancel(this._timeout);
 
     // in test mode return early if store cannot be found
     if (Ember.testing) {
@@ -51,6 +53,6 @@ export default class WidgetGlue {
   }
 
   cleanUp() {
-    Ember.run.cancel(this._timeout);
+    cancel(this._timeout);
   }
 }
diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
index 4b259063505..b11689796a6 100644
--- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import { createWidget, applyDecorators } from "discourse/widgets/widget";
 import { h } from "virtual-dom";
 import DiscourseURL from "discourse/lib/url";
@@ -342,7 +343,7 @@ export default createWidget("hamburger-menu", {
       const $headerCloak = $(".header-cloak");
       $headerCloak.addClass("animate");
       $headerCloak.css("opacity", 0);
-      Ember.run.later(() => this.sendWidgetAction("toggleHamburger"), 200);
+      later(() => this.sendWidgetAction("toggleHamburger"), 200);
     }
   },
 
diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6
index 0d1d30cc9aa..02fd704dca3 100644
--- a/app/assets/javascripts/discourse/widgets/header.js.es6
+++ b/app/assets/javascripts/discourse/widgets/header.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import { createWidget } from "discourse/widgets/widget";
 import { iconNode } from "discourse-common/lib/icon-library";
 import { avatarImg } from "discourse/widgets/post";
@@ -419,7 +420,7 @@ export default createWidget("header", {
     this.updateHighlight();
 
     if (this.state.searchVisible) {
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         const $searchInput = $("#search-term");
         $searchInput.focus().select();
 
diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
index c44035f76cd..3c70593130a 100644
--- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
@@ -1,3 +1,5 @@
+import { run } from "@ember/runloop";
+import { next } from "@ember/runloop";
 import { applyDecorators, createWidget } from "discourse/widgets/widget";
 import { avatarAtts } from "discourse/widgets/actions-summary";
 import { h } from "virtual-dom";
@@ -7,7 +9,7 @@ const LIKE_ACTION = 2;
 
 function animateHeart($elem, start, end, complete) {
   if (Ember.testing) {
-    return Ember.run(this, complete);
+    return run(this, complete);
   }
 
   $elem
@@ -411,7 +413,7 @@ export default createWidget("post-menu", {
       const likedPostId = keyValueStore.getInt("likedPostId");
       if (likedPostId === attrs.id) {
         keyValueStore.remove("likedPostId");
-        Ember.run.next(() => this.sendWidgetAction("toggleLike"));
+        next(() => this.sendWidgetAction("toggleLike"));
       }
     }
 
diff --git a/app/assets/javascripts/discourse/widgets/post-stream.js.es6 b/app/assets/javascripts/discourse/widgets/post-stream.js.es6
index 9291d8a4e67..6406d1c9e81 100644
--- a/app/assets/javascripts/discourse/widgets/post-stream.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-stream.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 import { createWidget } from "discourse/widgets/widget";
 import transformPost from "discourse/lib/transform-post";
 import { Placeholder } from "discourse/lib/posts-with-placeholders";
@@ -39,7 +40,7 @@ export function cloak(post, component) {
   _heights[post.id] = $post.outerHeight();
 
   component.dirtyKeys.keyDirty(`post-${post.id}`);
-  Ember.run.debounce(component, "queueRerender", 1000);
+  debounce(component, "queueRerender", 1000);
 }
 
 export function uncloak(post, component) {
diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6
index 808f3282c3e..1bd428a8352 100644
--- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6
@@ -1,3 +1,5 @@
+import { debounce } from "@ember/runloop";
+import { later } from "@ember/runloop";
 import { popupAjaxError } from "discourse/lib/ajax-error";
 import { searchForTerm, isValidSearchTerm } from "discourse/lib/search";
 import { createWidget } from "discourse/widgets/widget";
@@ -30,7 +32,7 @@ const SearchHelper = {
     }
 
     this._cancelSearch = true;
-    Ember.run.later(() => (this._cancelSearch = false), 400);
+    later(() => (this._cancelSearch = false), 400);
   },
 
   perform(widget) {
@@ -285,7 +287,7 @@ export default createWidget("search-menu", {
     searchData.noResults = false;
     this.searchService().set("highlightTerm", searchData.term);
     searchData.loading = true;
-    Ember.run.debounce(SearchHelper, SearchHelper.perform, this, 400);
+    debounce(SearchHelper, SearchHelper.perform, this, 400);
   },
 
   moreOfType(type) {
diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6
index eac0ac7b5c6..813a390ec51 100644
--- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6
+++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import { createWidget } from "discourse/widgets/widget";
 import ComponentConnector from "discourse/widgets/component-connector";
 import { h } from "virtual-dom";
@@ -400,7 +401,7 @@ export default createWidget("topic-timeline", {
     const stream = this.attrs.topic.get("postStream");
 
     // a little debounce to avoid flashing
-    Ember.run.later(() => {
+    later(() => {
       if (!this.state.position === scrollPosition) {
         return;
       }
diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6
index 96e56fb7acd..cf669d1edc4 100644
--- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import { createWidget } from "discourse/widgets/widget";
 import { h } from "virtual-dom";
 import { formatUsername } from "discourse/lib/utilities";
@@ -227,7 +228,7 @@ export default createWidget("user-menu", {
       const $headerCloak = $(".header-cloak");
       $headerCloak.addClass("animate");
       $headerCloak.css("opacity", 0);
-      Ember.run.later(() => this.sendWidgetAction("toggleUserMenu"), 200);
+      later(() => this.sendWidgetAction("toggleUserMenu"), 200);
     }
   },
 
diff --git a/app/assets/javascripts/pretty-text/oneboxer.js.es6 b/app/assets/javascripts/pretty-text/oneboxer.js.es6
index 4753ec80158..a500274a777 100644
--- a/app/assets/javascripts/pretty-text/oneboxer.js.es6
+++ b/app/assets/javascripts/pretty-text/oneboxer.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 let timeout;
 const loadingQueue = [];
 let localCache = {};
@@ -82,7 +83,7 @@ function loadNext(ajax) {
       }
     )
     .finally(() => {
-      timeout = Ember.run.later(() => loadNext(ajax), timeoutMs);
+      timeout = later(() => loadNext(ajax), timeoutMs);
       if (removeLoading) {
         $elem.removeClass(LOADING_ONEBOX_CSS_CLASS);
         $elem.data("onebox-loaded");
@@ -129,7 +130,7 @@ export function load({
   if (synchronous) {
     return loadNext(ajax);
   } else {
-    timeout = timeout || Ember.run.later(() => loadNext(ajax), 150);
+    timeout = timeout || later(() => loadNext(ajax), 150);
   }
 }
 
diff --git a/app/assets/javascripts/pretty-text/upload-short-url.js.es6 b/app/assets/javascripts/pretty-text/upload-short-url.js.es6
index 8b0b4db2b4d..3a2f46805c5 100644
--- a/app/assets/javascripts/pretty-text/upload-short-url.js.es6
+++ b/app/assets/javascripts/pretty-text/upload-short-url.js.es6
@@ -1,3 +1,4 @@
+import { debounce } from "@ember/runloop";
 let _cache = {};
 
 export function lookupCachedUploadUrl(shortUrl) {
@@ -100,7 +101,7 @@ export function resolveAllShortUrls(ajax) {
     $shortUploadUrls = $(attributes);
     if ($shortUploadUrls.length > 0) {
       // this is carefully batched so we can do a leading debounce (trigger right away)
-      return Ember.run.debounce(
+      return debounce(
         null,
         () => _loadShortUrls($shortUploadUrls, ajax),
         450,
diff --git a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6
index b086aa2e434..62522d9edd3 100644
--- a/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6
+++ b/app/assets/javascripts/select-kit/mixins/dom-helpers.js.es6
@@ -1,3 +1,5 @@
+import { next } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import { on } from "ember-addons/ember-computed-decorators";
 
 export default Ember.Mixin.create({
@@ -92,8 +94,8 @@ export default Ember.Mixin.create({
   focusFilterOrHeader() {
     const context = this;
     // next so we are sure it finised expand/collapse
-    Ember.run.next(() => {
-      Ember.run.schedule("afterRender", () => {
+    next(() => {
+      schedule("afterRender", () => {
         if (
           !context.$filterInput() ||
           !context.$filterInput().is(":visible") ||
@@ -129,9 +131,9 @@ export default Ember.Mixin.create({
     this.focusFilterOrHeader();
     this.autoHighlight();
 
-    Ember.run.next(() => {
+    next(() => {
       this._boundaryActionHandler("onExpand", this);
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         if (!this.isDestroying && !this.isDestroyed) {
           this._adjustPosition();
         }
@@ -144,9 +146,9 @@ export default Ember.Mixin.create({
 
     this.set("isExpanded", false);
 
-    Ember.run.next(() => {
+    next(() => {
       this._boundaryActionHandler("onCollapse", this);
-      Ember.run.schedule("afterRender", () => {
+      schedule("afterRender", () => {
         if (!this.isDestroying && !this.isDestroyed) {
           this._removeFixedPosition();
         }
diff --git a/app/assets/javascripts/select-kit/mixins/events.js.es6 b/app/assets/javascripts/select-kit/mixins/events.js.es6
index 0cbdf5da67c..f985da16166 100644
--- a/app/assets/javascripts/select-kit/mixins/events.js.es6
+++ b/app/assets/javascripts/select-kit/mixins/events.js.es6
@@ -1,3 +1,5 @@
+import { throttle } from "@ember/runloop";
+import { schedule } from "@ember/runloop";
 import { on } from "ember-addons/ember-computed-decorators";
 
 const { bind } = Ember.run;
@@ -138,7 +140,7 @@ export default Ember.Mixin.create({
       this.set("renderedFilterOnce", true);
     }
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this.$filterInput()
         .focus()
         .val(this.$filterInput().val() + String.fromCharCode(keyCode));
@@ -267,7 +269,7 @@ export default Ember.Mixin.create({
 
     const direction = keyCode === 38 ? -1 : 1;
 
-    Ember.run.throttle(this, this._moveHighlight, direction, $rows, 32);
+    throttle(this, this._moveHighlight, direction, $rows, 32);
   },
 
   didPressBackspaceFromFilter(event) {
@@ -428,7 +430,7 @@ export default Ember.Mixin.create({
   },
 
   _highlightRow($row) {
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       $row.trigger("mouseover").focus();
       this.focus();
     });
diff --git a/app/assets/javascripts/wizard/components/invite-list.js.es6 b/app/assets/javascripts/wizard/components/invite-list.js.es6
index e8b25973a62..de04d70bab5 100644
--- a/app/assets/javascripts/wizard/components/invite-list.js.es6
+++ b/app/assets/javascripts/wizard/components/invite-list.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 
 export default Component.extend({
@@ -60,7 +61,7 @@ export default Component.extend({
       this.updateField();
 
       this.set("inviteEmail", "");
-      Ember.run.scheduleOnce("afterRender", () =>
+      scheduleOnce("afterRender", () =>
         this.element.querySelector(".invite-email").focus()
       );
     },
diff --git a/app/assets/javascripts/wizard/components/radio-button.js.es6 b/app/assets/javascripts/wizard/components/radio-button.js.es6
index b40d9839f12..e7ceaa898f7 100644
--- a/app/assets/javascripts/wizard/components/radio-button.js.es6
+++ b/app/assets/javascripts/wizard/components/radio-button.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import Component from "@ember/component";
 import { observes, on } from "ember-addons/ember-computed-decorators";
 
@@ -13,7 +14,7 @@ export default Component.extend({
   @on("init")
   updateVal() {
     const checked = this.value === this.radioValue;
-    Ember.run.next(
+    next(
       () => (this.element.querySelector("input[type=radio]").checked = checked)
     );
   }
diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6
index c938d1e1672..1d034d1c47b 100644
--- a/app/assets/javascripts/wizard/components/wizard-step.js.es6
+++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 import getUrl from "discourse-common/lib/get-url";
 import {
@@ -92,7 +93,7 @@ export default Component.extend({
   },
 
   autoFocus() {
-    Ember.run.scheduleOnce("afterRender", () => {
+    scheduleOnce("afterRender", () => {
       const $invalid = $(".wizard-field.invalid:eq(0) .wizard-focusable");
 
       if ($invalid.length) {
@@ -104,7 +105,7 @@ export default Component.extend({
   },
 
   animateInvalidFields() {
-    Ember.run.scheduleOnce("afterRender", () =>
+    scheduleOnce("afterRender", () =>
       $(".invalid input[type=text], .invalid textarea").wiggle(2, 100)
     );
   },
diff --git a/app/assets/javascripts/wizard/lib/ajax.js.es6 b/app/assets/javascripts/wizard/lib/ajax.js.es6
index 9112c3787ff..64cc9571cc7 100644
--- a/app/assets/javascripts/wizard/lib/ajax.js.es6
+++ b/app/assets/javascripts/wizard/lib/ajax.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import getUrl from "discourse-common/lib/get-url";
 import jQuery from "jquery";
 
@@ -14,8 +15,8 @@ export function getToken() {
 export function ajax(args) {
   return new Ember.RSVP.Promise((resolve, reject) => {
     args.headers = { "X-CSRF-Token": getToken() };
-    args.success = data => Ember.run(null, resolve, data);
-    args.error = xhr => Ember.run(null, reject, xhr);
+    args.success = data => run(null, resolve, data);
+    args.error = xhr => run(null, reject, xhr);
     args.url = getUrl(args.url);
     jQuery.ajax(args);
   });
diff --git a/app/assets/javascripts/wizard/lib/preview.js.es6 b/app/assets/javascripts/wizard/lib/preview.js.es6
index 0ee5a35b276..d597bfae53f 100644
--- a/app/assets/javascripts/wizard/lib/preview.js.es6
+++ b/app/assets/javascripts/wizard/lib/preview.js.es6
@@ -1,3 +1,4 @@
+import { scheduleOnce } from "@ember/runloop";
 import Component from "@ember/component";
 /*eslint no-bitwise:0 */
 import getUrl from "discourse-common/lib/get-url";
@@ -72,7 +73,7 @@ export function createPreviewComponent(width, height, obj) {
       },
 
       triggerRepaint() {
-        Ember.run.scheduleOnce("afterRender", this, "repaint");
+        scheduleOnce("afterRender", this, "repaint");
       },
 
       repaint() {
diff --git a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6 b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
index 1a4177542ab..70a7e67fc06 100644
--- a/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
+++ b/app/assets/javascripts/wizard/test/acceptance/wizard-test.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import startApp from "wizard/test/helpers/start-app";
 
 var wizard;
@@ -7,7 +8,7 @@ QUnit.module("Acceptance: wizard", {
   },
 
   afterEach() {
-    Ember.run(wizard, "destroy");
+    run(wizard, "destroy");
   }
 });
 
diff --git a/app/assets/javascripts/wizard/test/helpers/start-app.js.es6 b/app/assets/javascripts/wizard/test/helpers/start-app.js.es6
index 233869bb1a2..d82b237c353 100644
--- a/app/assets/javascripts/wizard/test/helpers/start-app.js.es6
+++ b/app/assets/javascripts/wizard/test/helpers/start-app.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import Wizard from "wizard/wizard";
 import initializer from "wizard/initializers/load-helpers";
 
@@ -5,7 +6,7 @@ let app;
 let started = false;
 
 export default function() {
-  Ember.run(() => (app = Wizard.create({ rootElement: "#ember-testing" })));
+  run(() => (app = Wizard.create({ rootElement: "#ember-testing" })));
 
   if (!started) {
     initializer.initialize(app);
diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6
index 6b2f7776f09..76e3d64088b 100644
--- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6
+++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6
@@ -1,3 +1,4 @@
+import { schedule } from "@ember/runloop";
 import Component from "@ember/component";
 /* global Pikaday:true */
 import { propertyNotEqual } from "discourse/lib/computed";
@@ -55,7 +56,7 @@ export default Component.extend({
     if (markup) {
       cookAsync(markup).then(result => {
         this.set("currentPreview", result);
-        Ember.run.schedule("afterRender", () =>
+        schedule("afterRender", () =>
           this.$(".preview .discourse-local-date").applyLocalDates()
         );
       });
@@ -409,7 +410,7 @@ export default Component.extend({
       date = null;
     }
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this._picker.setMinDate(moment(date, this.dateFormat).toDate());
     });
   },
@@ -419,7 +420,7 @@ export default Component.extend({
       date = null;
     }
 
-    Ember.run.schedule("afterRender", () => {
+    schedule("afterRender", () => {
       this._picker.setDate(moment.utc(date), true);
     });
   },
diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
index c6818bb2ce8..2e6521008e2 100644
--- a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
+++ b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6
@@ -1,3 +1,6 @@
+import { once } from "@ember/runloop";
+import { debounce } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
 import Component from "@ember/component";
 import { ajax } from "discourse/lib/ajax";
 import {
@@ -26,12 +29,12 @@ export default Component.extend({
   @on("didInsertElement")
   composerOpened() {
     this._lastPublish = new Date();
-    Ember.run.once(this, "updateState");
+    once(this, "updateState");
   },
 
   @observes("action", "post.id", "topic.id")
   composerStateChanged() {
-    Ember.run.once(this, "updateState");
+    once(this, "updateState");
   },
 
   @observes("reply", "title")
@@ -44,8 +47,8 @@ export default Component.extend({
   @on("willDestroyElement")
   composerClosing() {
     this.publish({ previous: this.currentState });
-    Ember.run.cancel(this._pingTimer);
-    Ember.run.cancel(this._clearTimer);
+    cancel(this._pingTimer);
+    cancel(this._clearTimer);
   },
 
   updateState() {
@@ -94,7 +97,7 @@ export default Component.extend({
         r.messagebus_channel,
         message => {
           if (!this.isDestroyed) this.set("presenceUsers", message.users);
-          this._clearTimer = Ember.run.debounce(
+          this._clearTimer = debounce(
             this,
             "clear",
             keepAliveDuration + bufferTime
diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6
index e6861cdf6ba..37abc00f301 100644
--- a/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6
+++ b/plugins/discourse-presence/assets/javascripts/discourse/components/topic-presence-display.js.es6
@@ -1,3 +1,5 @@
+import { debounce } from "@ember/runloop";
+import { cancel } from "@ember/runloop";
 import Component from "@ember/component";
 import {
   default as computed,
@@ -26,7 +28,7 @@ export default Component.extend({
       this.channel,
       message => {
         if (!this.isDestroyed) this.set("presenceUsers", message.users);
-        this._clearTimer = Ember.run.debounce(
+        this._clearTimer = debounce(
           this,
           "clear",
           keepAliveDuration + bufferTime
@@ -38,7 +40,7 @@ export default Component.extend({
 
   @on("willDestroyElement")
   _destroyed() {
-    Ember.run.cancel(this._clearTimer);
+    cancel(this._clearTimer);
     this.messageBus.unsubscribe(this.channel);
   },
 
diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6
index 262146ab3d4..39573aa31be 100644
--- a/test/javascripts/acceptance/composer-test.js.es6
+++ b/test/javascripts/acceptance/composer-test.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import selectKit from "helpers/select-kit-helper";
 import { acceptance } from "helpers/qunit-helpers";
 import { toggleCheckDraftPopup } from "discourse/controllers/composer";
@@ -84,7 +85,7 @@ QUnit.test("Tests the Composer controls", async assert => {
   event[mac ? "metaKey" : "ctrlKey"] = true;
   event.keyCode = 66;
 
-  Ember.run(() => textarea.dispatchEvent(event));
+  run(() => textarea.dispatchEvent(event));
 
   const example = I18n.t(`composer.bold_text`);
   assert.equal(
diff --git a/test/javascripts/acceptance/modal-test.js.es6 b/test/javascripts/acceptance/modal-test.js.es6
index 45a71146721..13725f4d6cc 100644
--- a/test/javascripts/acceptance/modal-test.js.es6
+++ b/test/javascripts/acceptance/modal-test.js.es6
@@ -1,3 +1,4 @@
+import { run } from "@ember/runloop";
 import { acceptance, controllerFor } from "helpers/qunit-helpers";
 import showModal from "discourse/lib/show-modal";
 
@@ -37,7 +38,7 @@ QUnit.test("modal", async function(assert) {
     '{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}'
   );
 
-  Ember.run(() => showModal("not-dismissable", {}));
+  run(() => showModal("not-dismissable", {}));
 
   assert.ok(find(".d-modal:visible").length === 1, "modal should appear");
 
diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6
index cb008f03233..8c6d936e89e 100644
--- a/test/javascripts/components/d-editor-test.js.es6
+++ b/test/javascripts/components/d-editor-test.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import componentTest from "helpers/component-test";
 import { withPluginApi } from "discourse/lib/plugin-api";
 import formatTextWithSelection from "helpers/d-editor-helper";
@@ -747,7 +748,7 @@ composerTestCase("replace-text event for composer", async function(assert) {
         .lookup("service:app-events")
         .trigger("composer:replace-text", "green", "yellow", { forceFocus: true });
 
-      Ember.run.next(() => {
+      next(() => {
         let expect = formatTextWithSelection(AFTER, CASE.after);
         let actual = formatTextWithSelection(
           this.value,
diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6
index f12da986660..695a2042911 100644
--- a/test/javascripts/controllers/topic-test.js.es6
+++ b/test/javascripts/controllers/topic-test.js.es6
@@ -1,4 +1,5 @@
 import EmberObject from "@ember/object";
+import { next } from "@ember/runloop";
 import Topic from "discourse/models/topic";
 import PostStream from "discourse/models/post-stream";
 import { Placeholder } from "discourse/lib/posts-with-placeholders";
@@ -545,7 +546,7 @@ QUnit.test(
     const done = assert.async();
     controller.send("deletePost", post);
 
-    Ember.run.next(() => {
+    next(() => {
       assert.ok(destroyed, "post was destroyed");
       done();
     });
diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6
index 5e79f87647a..ca1bd8363a4 100644
--- a/test/javascripts/helpers/qunit-helpers.js.es6
+++ b/test/javascripts/helpers/qunit-helpers.js.es6
@@ -1,3 +1,5 @@
+import { run } from "@ember/runloop";
+import { later } from "@ember/runloop";
 /* global QUnit, resetSite */
 
 import sessionFixtures from "fixtures/session-fixtures";
@@ -155,7 +157,7 @@ export function controllerFor(controller, model) {
 export function asyncTestDiscourse(text, func) {
   QUnit.test(text, function(assert) {
     const done = assert.async();
-    Ember.run(() => {
+    run(() => {
       func.call(this, assert);
       done();
     });
@@ -206,7 +208,7 @@ export function waitFor(assert, callback, timeout) {
   timeout = timeout || 500;
 
   const done = assert.async();
-  Ember.run.later(() => {
+  later(() => {
     callback();
     done();
   }, timeout);
diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6
index d64823cd2a3..a40e803bd2e 100644
--- a/test/javascripts/lib/click-track-test.js.es6
+++ b/test/javascripts/lib/click-track-test.js.es6
@@ -1,3 +1,4 @@
+import { later } from "@ember/runloop";
 import DiscourseURL from "discourse/lib/url";
 import ClickTrack from "discourse/lib/click-track";
 import { fixture, logIn } from "helpers/qunit-helpers";
@@ -173,7 +174,7 @@ QUnit.skip("restores the href after a while", async assert => {
   assert.timeout(75);
 
   const done = assert.async();
-  Ember.run.later(() => {
+  later(() => {
     assert.equal(fixture("a").attr("href"), "http://www.google.com");
     done();
   });
diff --git a/test/javascripts/models/nav-item-test.js.es6 b/test/javascripts/models/nav-item-test.js.es6
index 115417e4d48..429b669d7ca 100644
--- a/test/javascripts/models/nav-item-test.js.es6
+++ b/test/javascripts/models/nav-item-test.js.es6
@@ -1,8 +1,9 @@
+import { run } from "@ember/runloop";
 import createStore from "helpers/create-store";
 
 QUnit.module("Discourse.NavItem", {
   beforeEach() {
-    Ember.run(function() {
+    run(function() {
       const asianCategory = Discourse.Category.create({
         name: "确实是这样",
         id: 343434
diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6
index fcb35f96b9a..4666285c278 100644
--- a/test/javascripts/widgets/widget-test.js.es6
+++ b/test/javascripts/widgets/widget-test.js.es6
@@ -1,3 +1,4 @@
+import { next } from "@ember/runloop";
 import { moduleForWidget, widgetTest } from "helpers/widget-test";
 import { createWidget } from "discourse/widgets/widget";
 import { withPluginApi } from "discourse/lib/plugin-api";
@@ -159,7 +160,7 @@ widgetTest("widget update with promise", {
 
       click() {
         return new Ember.RSVP.Promise(resolve => {
-          Ember.run.next(() => {
+          next(() => {
             this.state.name = "Robin";
             resolve();
           });