diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js
index 7e524a26f4..aeb615c22f 100644
--- a/awx/ui/client/features/output/index.controller.js
+++ b/awx/ui/client/features/output/index.controller.js
@@ -1,6 +1,3 @@
-const JOB_START = 'playbook_on_start';
-const JOB_END = 'playbook_on_stats';
-
let vm;
let $compile;
let $scope;
@@ -8,17 +5,20 @@ let $q;
let page;
let render;
let scroll;
+let stream;
let resource;
let $state;
let qs;
let chain;
+let chainLength;
function JobsIndexController (
_resource_,
_page_,
_scroll_,
_render_,
+ _stream_,
_$scope_,
_$compile_,
_$q_,
@@ -35,6 +35,7 @@ function JobsIndexController (
page = _page_;
scroll = _scroll_;
render = _render_;
+ stream = _stream_;
// Development helper(s)
vm.clear = devClear;
@@ -53,17 +54,6 @@ function JobsIndexController (
vm.expand = expand;
vm.isExpanded = true;
- // Real-time (active between JOB_START and JOB_END events only)
- vm.stream = {
- active: false,
- rendering: false,
- paused: false
- };
-
- const stream = false; // TODO: Set in route
-
- chain = $q.resolve();
-
// search
$state = _$state_;
qs = _qs_;
@@ -83,8 +73,10 @@ function JobsIndexController (
render.requestAnimationFrame(() => init());
}
-function init (stream) {
- page.init(resource);
+function init (pageMode) {
+ page.init({
+ resource
+ });
render.init({
get: () => resource.model.get(`related.${resource.related}.results`),
@@ -97,76 +89,24 @@ function init (stream) {
next
});
- if (stream) {
- $scope.$on(resource.ws.namespace, process);
- } else {
+ stream.init({
+ page,
+ scroll,
+ resource,
+ render: events => shift().then(() => append(events, true)),
+ listen: (namespace, listener) => {
+ $scope.$on(namespace, (scope, data) => listener(data));
+ }
+ });
+
+ if (pageMode) {
next();
}
}
-function process (scope, data) {
- chain = chain.then(() => {
- if (data.event === JOB_START) {
- vm.stream.active = true;
- scroll.lock();
- } else if (data.event === JOB_END) {
- vm.stream.active = false;
- }
-
- const pageAdded = page.addToBuffer(data);
-
- if (pageAdded && !scroll.isLocked()) {
- vm.stream.paused = true;
- }
-
- if (vm.stream.paused && scroll.isLocked()) {
- vm.stream.paused = false;
- }
-
- if (vm.stream.rendering || vm.stream.paused) {
- return;
- }
-
- const events = page.emptyBuffer();
-
- return renderStream(events);
- })
-}
-
-function renderStream (events) {
- vm.stream.rendering = true;
-
- return shift()
- .then(() => append(events, true))
- .then(() => {
- if (scroll.isLocked()) {
- scroll.setScrollPosition(scroll.getScrollHeight());
- }
-
- if (!vm.stream.active) {
- const buffer = page.emptyBuffer();
-
- if (buffer.length) {
- return renderStream(buffer);
- } else {
- vm.stream.rendering = false;
- scroll.unlock();
- }
- } else {
- vm.stream.rendering = false;
- }
- });
-}
-
-function devClear () {
- init(true);
+function devClear (pageMode) {
+ init(pageMode);
render.clear();
-
- vm.stream = {
- active: false,
- rendering: false,
- paused: false
- };
}
function next () {
@@ -257,14 +197,12 @@ function scrollHome () {
}
function scrollEnd () {
- if (scroll.isLocked()) {
- page.setBookmark();
- scroll.unlock();
-
- return;
- } else if (!scroll.isLocked() && vm.stream.active) {
- page.removeBookmark();
- scroll.lock();
+ if (stream.isActive()) {
+ if (stream.isPaused()) {
+ stream.resume();
+ } else {
+ stream.pause();
+ }
return;
}
@@ -339,6 +277,7 @@ function toggle (uuid, menu) {
lines.removeClass('hidden');
}
+}
//
// Search
@@ -402,6 +341,7 @@ JobsIndexController.$inject = [
'JobPageService',
'JobScrollService',
'JobRenderService',
+ 'JobStreamService',
'$scope',
'$compile',
'$q',
diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js
index 593b7b1097..64819e9ca3 100644
--- a/awx/ui/client/features/output/index.js
+++ b/awx/ui/client/features/output/index.js
@@ -7,6 +7,7 @@ import PageService from '~features/output/page.service';
import RenderService from '~features/output/render.service';
import ScrollService from '~features/output/scroll.service';
import SearchKeyDirective from '~features/output/search-key.directive';
+import StreamService from '~features/output/stream.service';
const Template = require('~features/output/index.view.html');
@@ -190,6 +191,7 @@ angular
.service('JobStrings', Strings)
.service('JobPageService', PageService)
.service('JobScrollService', ScrollService)
+ .service('JobStreamService', StreamService)
.directive('atSearchKey', SearchKeyDirective)
.run(JobsRun);
diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html
index 2516ede6c7..53b69a43b2 100644
--- a/awx/ui/client/features/output/index.view.html
+++ b/awx/ui/client/features/output/index.view.html
@@ -1,7 +1,8 @@
-
+
+
diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js
index a89d56d6b8..64f0542fe0 100644
--- a/awx/ui/client/features/output/page.service.js
+++ b/awx/ui/client/features/output/page.service.js
@@ -1,5 +1,5 @@
function JobPageService ($q) {
- this.init = resource => {
+ this.init = ({ resource }) => {
this.resource = resource;
this.page = {
@@ -54,12 +54,9 @@ function JobPageService ($q) {
reference.state.count++;
};
- this.addToPageCache = (index, event, reference) => {
- reference.cache[index].events.push(event);
- };
-
this.addToBuffer = event => {
const reference = this.getReference();
+ const index = reference.cache.length - 1;
let pageAdded = false;
if (this.result.count % this.page.size === 0) {
@@ -70,9 +67,10 @@ function JobPageService ($q) {
}
this.trimBuffer();
+
pageAdded = true;
} else {
- this.addToPageCache(reference.cache.length - 1, event, reference);
+ reference.cache[index].events.push(event);
}
this.buffer.count++;
@@ -97,6 +95,14 @@ function JobPageService ($q) {
}
};
+ this.isBufferFull = () => {
+ if (this.buffer.count === 2) {
+ return true;
+ }
+
+ return false;
+ }
+
this.emptyBuffer = () => {
const reference = this.getReference();
let data = [];
@@ -183,9 +189,9 @@ function JobPageService ($q) {
return;
}
- this.bookmark.state.first = this.page.state.first;
- this.bookmark.state.last = this.page.state.last;
- this.bookmark.state.current = this.page.state.current;
+ this.bookmark.state.first = this.page.state.first - 1;
+ this.bookmark.state.last = this.page.state.last - 1;
+ this.bookmark.state.current = this.page.state.current - 1;
this.bookmark.cache = JSON.parse(JSON.stringify(this.page.cache));
this.bookmark.set = true;
this.bookmark.pending = false;
diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js
new file mode 100644
index 0000000000..557337adc3
--- /dev/null
+++ b/awx/ui/client/features/output/stream.service.js
@@ -0,0 +1,182 @@
+const JOB_START = 'playbook_on_start';
+const JOB_END = 'playbook_on_stats';
+const MAX_LAG = 120;
+
+function JobStreamService ($q) {
+ this.init = ({ resource, scroll, page, render, listen }) => {
+ this.resource = resource;
+ this.scroll = scroll;
+ this.page = page;
+
+ this.lag = 0;
+ this.count = 0;
+ this.pageCount = 0;
+ this.chain = $q.resolve();
+ this.factors = this.getBatchFactors(this.resource.page.size);
+ this.state = {
+ started: false,
+ paused: false,
+ pausing: false,
+ resuming: false,
+ ending: false,
+ ended: false
+ };
+
+ this.hooks = {
+ render,
+ listen
+ };
+
+ this.hooks.listen(resource.ws.namespace, this.listen);
+ };
+
+ this.getBatchFactors = size => {
+ const factors = [1];
+
+ for (let i = 2; i <= size / 2; i++) {
+ if (size % i === 0) {
+ factors.push(i);
+ }
+ }
+
+ factors.push(size);
+
+ return factors;
+ };
+
+ this.getBatchFactorIndex = () => {
+ const index = Math.floor((this.lag / MAX_LAG) * this.factors.length);
+
+ return index > this.factors.length - 1 ? this.factors.length - 1 : index;
+ };
+
+ this.setBatchFrameCount = () => {
+ const index = this.getBatchFactorIndex();
+
+ this.framesPerRender = this.factors[index];
+ };
+
+ this.buffer = data => {
+ const pageAdded = this.page.addToBuffer(data);
+
+ this.pageCount++;
+
+ if (pageAdded) {
+ this.setBatchFrameCount();
+
+ if (this.isPausing()) {
+ this.pause(true);
+ } else if (this.isResuming()) {
+ this.resume(true);
+ }
+ }
+ };
+
+ this.listen = data => {
+ this.lag++;
+
+ this.chain = this.chain
+ .then(() => {
+ if (data.event === JOB_START) {
+ this.start();
+ } else if (data.event === JOB_END) {
+ if (this.isPaused()) {
+ this.end(true);
+ } else {
+ this.end();
+ }
+ }
+
+ this.buffer(data);
+ this.count++;
+
+ if (this.isPaused() || !this.isBatchFull()) {
+ return $q.resolve();
+ }
+
+ const events = this.page.emptyBuffer();
+ this.count -= events.length;
+
+ return this.renderFrame(events);
+ })
+ .then(() => --this.lag);
+ };
+
+ this.renderFrame = events => {
+ return this.hooks.render(events)
+ .then(() => {
+ if (this.scroll.isLocked()) {
+ this.scroll.setScrollPosition(this.scroll.getScrollHeight());
+ }
+
+ if (this.isEnding()) {
+ const lastEvents = this.page.emptyBuffer();
+
+ if (lastEvents.length) {
+ return this.renderFrame(lastEvents);
+ }
+
+ this.end(true);
+ }
+
+ return $q.resolve();
+ });
+ };
+
+ this.resume = done => {
+ if (done) {
+ this.state.resuming = false;
+ this.state.paused = false;
+
+ return;
+ }
+
+ this.scroll.lock();
+ this.state.resuming = true;
+ this.page.removeBookmark();
+ };
+
+ this.pause = done => {
+ if (done) {
+ this.state.pausing = false;
+ this.state.paused = true;
+ this.scroll.resume();
+
+ return;
+ }
+
+ this.scroll.unlock();
+ this.scroll.pause();
+ this.state.pausing = true;
+ this.page.setBookmark();
+ };
+
+ this.start = () => {
+ this.state.started = true;
+ this.scroll.lock();
+ };
+
+ this.end = done => {
+ if (done) {
+ this.state.ending = false;
+ this.state.ended = true;
+ this.scroll.unlock();
+
+ return;
+ }
+
+ this.state.ending = true;
+ };
+
+ this.isBatchFull = () => this.count % this.framesPerRender === 0;
+ this.isPaused = () => this.state.paused;
+ this.isPausing = () => this.state.pausing;
+ this.isResuming = () => this.state.resuming;
+ this.isActive = () => this.state.started && !this.state.ended;
+ this.isEnding = () => this.state.ending;
+ this.isDone = () => this.state.ended;
+}
+
+JobStreamService.$inject = ['$q'];
+
+export default JobStreamService;