Merge pull request #2880 from jakemcdermott/fix-2828

add event discard with interactive discontinuities for high volume jobs
This commit is contained in:
Jake McDermott
2018-08-29 04:01:04 -04:00
committed by GitHub
7 changed files with 897 additions and 723 deletions

View File

@@ -104,6 +104,10 @@
user-select: none; user-select: none;
} }
&-line--clickable {
cursor: pointer;
}
&-event { &-event {
.at-mixin-event(); .at-mixin-event();
} }

View File

@@ -16,6 +16,7 @@ export const JOB_STATUS_FINISHED = JOB_STATUS_COMPLETE.concat(JOB_STATUS_INCOMPL
export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container'; export const OUTPUT_ELEMENT_CONTAINER = '.at-Stdout-container';
export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable'; export const OUTPUT_ELEMENT_TBODY = '#atStdoutResultTable';
export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast'; export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast';
export const OUTPUT_MAX_BUFFER_LENGTH = 1000;
export const OUTPUT_MAX_LAG = 120; export const OUTPUT_MAX_LAG = 120;
export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update']; export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update'];
export const OUTPUT_ORDER_BY = 'counter'; export const OUTPUT_ORDER_BY = 'counter';

View File

@@ -17,61 +17,21 @@ let scroll;
let status; let status;
let slide; let slide;
let stream; let stream;
let page;
let vm; let vm;
const bufferState = [0, 0]; // [length, count]
const listeners = []; const listeners = [];
const rx = []; let lockFrames = false;
function bufferInit () {
rx.length = 0;
bufferState[0] = 0;
bufferState[1] = 0;
}
function bufferAdd (event) {
rx.push(event);
bufferState[0] += 1;
bufferState[1] += 1;
return bufferState[1];
}
function bufferEmpty (min, max) {
let count = 0;
let removed = [];
for (let i = bufferState[0] - 1; i >= 0; i--) {
if (rx[i].counter <= max) {
removed = removed.concat(rx.splice(i, 1));
count++;
}
}
bufferState[0] -= count;
return removed;
}
let lockFrames;
function onFrames (events) { function onFrames (events) {
if (lockFrames) {
events.forEach(bufferAdd);
return $q.resolve();
}
events = slide.pushFrames(events); events = slide.pushFrames(events);
const popCount = events.length - slide.getCapacity();
const isAttached = events.length > 0;
if (!isAttached) { if (lockFrames) {
stopFollowing();
return $q.resolve(); return $q.resolve();
} }
const popCount = events.length - render.getCapacity();
if (!vm.isFollowing && canStartFollowing()) { if (!vm.isFollowing && canStartFollowing()) {
startFollowing(); startFollowing();
} }
@@ -86,13 +46,13 @@ function onFrames (events) {
scroll.scrollToBottom(); scroll.scrollToBottom();
} }
return slide.popBack(popCount) return render.popBack(popCount)
.then(() => { .then(() => {
if (vm.isFollowing) { if (vm.isFollowing) {
scroll.scrollToBottom(); scroll.scrollToBottom();
} }
return slide.pushFront(events); return render.pushFront(events);
}) })
.then(() => { .then(() => {
if (vm.isFollowing) { if (vm.isFollowing) {
@@ -105,27 +65,44 @@ function onFrames (events) {
}); });
} }
function first () { //
// Menu Controls (Running)
//
function firstRange () {
if (scroll.isPaused()) { if (scroll.isPaused()) {
return $q.resolve(); return $q.resolve();
} }
stopFollowing();
lockFollow = true;
if (slide.isOnFirstPage()) {
scroll.resetScrollPosition();
return $q.resolve();
}
scroll.pause(); scroll.pause();
lockFrames = true; lockFrames = true;
stopFollowing(); return render.clear()
.then(() => slide.getFirst())
.then(results => render.pushFront(results))
.then(() => slide.getNext())
.then(results => {
const popCount = results.length - render.getCapacity();
return slide.getFirst() return render.popBack(popCount)
.then(() => { .then(() => render.pushFront(results));
scroll.resetScrollPosition();
}) })
.finally(() => { .finally(() => {
scroll.resume(); scroll.resume();
lockFrames = false; lockFollow = false;
}); });
} }
function next () { function nextRange () {
if (vm.isFollowing) { if (vm.isFollowing) {
scroll.scrollToBottom(); scroll.scrollToBottom();
@@ -136,34 +113,49 @@ function next () {
return $q.resolve(); return $q.resolve();
} }
if (slide.getTailCounter() >= slide.getMaxCounter()) {
return $q.resolve();
}
scroll.pause(); scroll.pause();
lockFrames = true; lockFrames = true;
return slide.getNext() return slide.getNext()
.then(results => {
const popCount = results.length - render.getCapacity();
return render.popBack(popCount)
.then(() => render.pushFront(results));
})
.finally(() => { .finally(() => {
scroll.resume(); scroll.resume();
lockFrames = false; lockFrames = false;
return $q.resolve();
}); });
} }
function previous () { function previousRange () {
if (scroll.isPaused()) { if (scroll.isPaused()) {
return $q.resolve(); return $q.resolve();
} }
scroll.pause(); scroll.pause();
stopFollowing();
lockFrames = true; lockFrames = true;
stopFollowing(); let initialPosition;
let popHeight;
const initialPosition = scroll.getScrollPosition();
return slide.getPrevious() return slide.getPrevious()
.then(popHeight => { .then(results => {
const popCount = results.length - render.getCapacity();
initialPosition = scroll.getScrollPosition();
return render.popFront(popCount)
.then(() => {
popHeight = scroll.getScrollHeight();
return render.pushBack(results);
});
})
.then(() => {
const currentHeight = scroll.getScrollHeight(); const currentHeight = scroll.getScrollHeight();
scroll.setScrollPosition(currentHeight - popHeight + initialPosition); scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
@@ -172,10 +164,12 @@ function previous () {
.finally(() => { .finally(() => {
scroll.resume(); scroll.resume();
lockFrames = false; lockFrames = false;
return $q.resolve();
}); });
} }
function last () { function lastRange () {
if (scroll.isPaused()) { if (scroll.isPaused()) {
return $q.resolve(); return $q.resolve();
} }
@@ -183,16 +177,39 @@ function last () {
scroll.pause(); scroll.pause();
lockFrames = true; lockFrames = true;
return slide.getLast() return render.clear()
.then(() => slide.getLast())
.then(results => render.pushFront(results))
.then(() => { .then(() => {
stream.setMissingCounterThreshold(slide.getTailCounter() + 1); stream.setMissingCounterThreshold(slide.getTailCounter() + 1);
scroll.scrollToBottom(); scroll.scrollToBottom();
lockFrames = false;
return $q.resolve(); return $q.resolve();
}) })
.finally(() => { .finally(() => {
scroll.resume(); scroll.resume();
lockFrames = false;
return $q.resolve();
});
}
function menuLastRange () {
if (vm.isFollowing) {
lockFollow = true;
stopFollowing();
return $q.resolve();
}
lockFollow = false;
return lastRange()
.then(() => {
startFollowing();
return $q.resolve();
}); });
} }
@@ -211,8 +228,7 @@ function canStartFollowing () {
if (followOnce && // one-time activation from top of first page if (followOnce && // one-time activation from top of first page
scroll.isBeyondUpperThreshold() && scroll.isBeyondUpperThreshold() &&
slide.getHeadCounter() === 1 && slide.getTailCounter() - slide.getHeadCounter() >= OUTPUT_PAGE_SIZE) {
slide.getTailCounter() >= OUTPUT_PAGE_SIZE) {
followOnce = false; followOnce = false;
return true; return true;
@@ -242,23 +258,159 @@ function stopFollowing () {
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST'); vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
} }
//
// Menu Controls (Page Mode)
//
function firstPage () {
if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return render.clear()
.then(() => page.getFirst())
.then(results => render.pushFront(results))
.then(() => page.getNext())
.then(results => {
const popCount = page.trimHead();
return render.popBack(popCount)
.then(() => render.pushFront(results));
})
.finally(() => {
scroll.resume();
return $q.resolve();
});
}
function lastPage () {
if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return render.clear()
.then(() => page.getLast())
.then(results => render.pushBack(results))
.then(() => page.getPrevious())
.then(results => {
const popCount = page.trimTail();
return render.popFront(popCount)
.then(() => render.pushBack(results));
})
.then(() => {
scroll.scrollToBottom();
return $q.resolve();
})
.finally(() => {
scroll.resume();
return $q.resolve();
});
}
function nextPage () {
if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
return page.getNext()
.then(results => {
const popCount = page.trimHead();
return render.popBack(popCount)
.then(() => render.pushFront(results));
})
.finally(() => {
scroll.resume();
});
}
function previousPage () {
if (scroll.isPaused()) {
return $q.resolve();
}
scroll.pause();
let initialPosition;
let popHeight;
return page.getPrevious()
.then(results => {
const popCount = page.trimTail();
initialPosition = scroll.getScrollPosition();
return render.popFront(popCount)
.then(() => {
popHeight = scroll.getScrollHeight();
return render.pushBack(results);
});
})
.then(() => {
const currentHeight = scroll.getScrollHeight();
scroll.setScrollPosition(currentHeight - popHeight + initialPosition);
return $q.resolve();
})
.finally(() => {
scroll.resume();
return $q.resolve();
});
}
//
// Menu Controls
//
function first () {
if (vm.isProcessingFinished) {
return firstPage();
}
return firstRange();
}
function last () {
if (vm.isProcessingFinished) {
return lastPage();
}
return lastRange();
}
function next () {
if (vm.isProcessingFinished) {
return nextPage();
}
return nextRange();
}
function previous () {
if (vm.isProcessingFinished) {
return previousPage();
}
return previousRange();
}
function menuLast () { function menuLast () {
if (vm.isFollowing) { if (vm.isProcessingFinished) {
lockFollow = true; return lastPage();
stopFollowing();
return $q.resolve();
} }
lockFollow = false; return menuLastRange();
if (slide.isOnLastPage()) {
scroll.scrollToBottom();
return $q.resolve();
}
return last();
} }
function down () { function down () {
@@ -273,6 +425,10 @@ function togglePanelExpand () {
vm.isPanelExpanded = !vm.isPanelExpanded; vm.isPanelExpanded = !vm.isPanelExpanded;
} }
//
// Line Interaction
//
const iconCollapsed = 'fa-angle-right'; const iconCollapsed = 'fa-angle-right';
const iconExpanded = 'fa-angle-down'; const iconExpanded = 'fa-angle-down';
const iconSelector = '.at-Stdout-toggle > i'; const iconSelector = '.at-Stdout-toggle > i';
@@ -281,7 +437,7 @@ const lineCollapsed = 'hidden';
function toggleCollapseAll () { function toggleCollapseAll () {
if (scroll.isPaused()) return; if (scroll.isPaused()) return;
const records = Object.keys(render.record).map(key => render.record[key]); const records = Object.keys(render.records).map(key => render.records[key]);
const plays = records.filter(({ name }) => name === EVENT_START_PLAY); const plays = records.filter(({ name }) => name === EVENT_START_PLAY);
const tasks = records.filter(({ name }) => name === EVENT_START_TASK); const tasks = records.filter(({ name }) => name === EVENT_START_TASK);
@@ -321,7 +477,7 @@ function toggleCollapseAll () {
function toggleCollapse (uuid) { function toggleCollapse (uuid) {
if (scroll.isPaused()) return; if (scroll.isPaused()) return;
const record = render.record[uuid]; const record = render.records[uuid];
if (record.name === EVENT_START_PLAY) { if (record.name === EVENT_START_PLAY) {
togglePlayCollapse(uuid); togglePlayCollapse(uuid);
@@ -333,7 +489,7 @@ function toggleCollapse (uuid) {
} }
function togglePlayCollapse (uuid) { function togglePlayCollapse (uuid) {
const record = render.record[uuid]; const record = render.records[uuid];
const descendants = record.children || []; const descendants = record.children || [];
const icon = $(`#${uuid} ${iconSelector}`); const icon = $(`#${uuid} ${iconSelector}`);
@@ -364,11 +520,11 @@ function togglePlayCollapse (uuid) {
} }
descendants descendants
.map(item => render.record[item]) .map(item => render.records[item])
.filter(({ name }) => name === EVENT_START_TASK) .filter(({ name }) => name === EVENT_START_TASK)
.forEach(rec => { render.record[rec.uuid].isCollapsed = true; }); .forEach(rec => { render.records[rec.uuid].isCollapsed = true; });
render.record[uuid].isCollapsed = !isCollapsed; render.records[uuid].isCollapsed = !isCollapsed;
} }
function toggleTaskCollapse (uuid) { function toggleTaskCollapse (uuid) {
@@ -387,7 +543,7 @@ function toggleTaskCollapse (uuid) {
lines.addClass(lineCollapsed); lines.addClass(lineCollapsed);
} }
render.record[uuid].isCollapsed = !isCollapsed; render.records[uuid].isCollapsed = !isCollapsed;
} }
function compile (html) { function compile (html) {
@@ -398,6 +554,60 @@ function showHostDetails (id, uuid) {
$state.go('output.host-event.json', { eventId: id, taskUuid: uuid }); $state.go('output.host-event.json', { eventId: id, taskUuid: uuid });
} }
function showMissingEvents (uuid) {
const record = render.records[uuid];
const min = Math.min(...record.counters);
const max = Math.min(Math.max(...record.counters), min + OUTPUT_PAGE_SIZE);
const selector = `#${uuid}`;
const clicked = $(selector);
return resource.events.getRange([min, max])
.then(results => {
const counters = results.map(({ counter }) => counter);
for (let i = min; i <= max; i++) {
if (counters.indexOf(i) < 0) {
results = results.filter(({ counter }) => counter < i);
break;
}
}
let lines = 0;
let untrusted = '';
for (let i = 0; i <= results.length - 1; i++) {
const { html, count } = render.transformEvent(results[i]);
lines += count;
untrusted += html;
const shifted = render.records[uuid].counters.shift();
delete render.uuids[shifted];
}
const trusted = render.trustHtml(untrusted);
const elements = angular.element(trusted);
return render
.requestAnimationFrame(() => {
elements.insertBefore(clicked);
if (render.records[uuid].counters.length === 0) {
clicked.remove();
delete render.records[uuid];
}
})
.then(() => render.compile(elements))
.then(() => lines);
});
}
//
// Event Handling
//
let streaming; let streaming;
function stopListening () { function stopListening () {
streaming = null; streaming = null;
@@ -420,7 +630,7 @@ function startListening () {
function handleJobEvent (data) { function handleJobEvent (data) {
streaming = streaming || resource.events streaming = streaming || resource.events
.getRange([Math.max(0, data.counter - 50), data.counter + 50]) .getRange([Math.max(1, data.counter - 50), data.counter + 50])
.then(results => { .then(results => {
results.push(data); results.push(data);
@@ -440,12 +650,13 @@ function handleJobEvent (data) {
results = results.filter(({ counter }) => counter > maxMissing); results = results.filter(({ counter }) => counter > maxMissing);
} }
stream.setMissingCounterThreshold(max);
results.forEach(item => { results.forEach(item => {
stream.pushJobEvent(item); stream.pushJobEvent(item);
status.pushJobEvent(item); status.pushJobEvent(item);
}); });
stream.setMissingCounterThreshold(min);
return $q.resolve(); return $q.resolve();
}); });
@@ -467,12 +678,20 @@ function handleSummaryEvent (data) {
stream.setFinalCounter(data.final_counter); stream.setFinalCounter(data.final_counter);
} }
//
// Search
//
function reloadState (params) { function reloadState (params) {
params.isPanelExpanded = vm.isPanelExpanded; params.isPanelExpanded = vm.isPanelExpanded;
return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' }); return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' });
} }
//
// Debug Mode
//
function clear () { function clear () {
stopListening(); stopListening();
render.clear(); render.clear();
@@ -481,9 +700,9 @@ function clear () {
lockFollow = false; lockFollow = false;
lockFrames = false; lockFrames = false;
bufferInit(); stream.bufferInit();
status.init(resource); status.init(resource);
slide.init(render, resource.events, scroll); slide.init(resource.events, render);
status.subscribe(data => { vm.status = data.status; }); status.subscribe(data => { vm.status = data.status; });
startListening(); startListening();
@@ -518,7 +737,8 @@ function OutputIndexController (
render = _render_; render = _render_;
status = _status_; status = _status_;
stream = _stream_; stream = _stream_;
slide = isProcessingFinished ? _page_ : _slide_; slide = _slide_;
page = _page_;
vm = this || {}; vm = this || {};
@@ -529,6 +749,7 @@ function OutputIndexController (
vm.resource = resource; vm.resource = resource;
vm.reloadState = reloadState; vm.reloadState = reloadState;
vm.isPanelExpanded = isPanelExpanded; vm.isPanelExpanded = isPanelExpanded;
vm.isProcessingFinished = isProcessingFinished;
vm.togglePanelExpand = togglePanelExpand; vm.togglePanelExpand = togglePanelExpand;
// Stdout Navigation // Stdout Navigation
@@ -538,16 +759,17 @@ function OutputIndexController (
vm.toggleCollapseAll = toggleCollapseAll; vm.toggleCollapseAll = toggleCollapseAll;
vm.toggleCollapse = toggleCollapse; vm.toggleCollapse = toggleCollapse;
vm.showHostDetails = showHostDetails; vm.showHostDetails = showHostDetails;
vm.showMissingEvents = showMissingEvents;
vm.toggleLineEnabled = resource.model.get('type') === 'job'; vm.toggleLineEnabled = resource.model.get('type') === 'job';
vm.followTooltip = vm.strings.get('tooltips.MENU_LAST'); vm.followTooltip = vm.strings.get('tooltips.MENU_LAST');
vm.debug = _debug; vm.debug = _debug;
render.requestAnimationFrame(() => { render.requestAnimationFrame(() => {
bufferInit(); render.init({ compile, toggles: vm.toggleLineEnabled });
status.init(resource); status.init(resource);
slide.init(render, resource.events, scroll); page.init(resource.events);
render.init({ compile, toggles: vm.toggleLineEnabled }); slide.init(resource.events, render);
scroll.init({ scroll.init({
next, next,
@@ -564,8 +786,6 @@ function OutputIndexController (
let showFollowTip = true; let showFollowTip = true;
const rates = []; const rates = [];
stream.init({ stream.init({
bufferAdd,
bufferEmpty,
onFrames, onFrames,
onFrameRate (rate) { onFrameRate (rate) {
rates.push(rate); rates.push(rate);
@@ -638,4 +858,3 @@ OutputIndexController.$inject = [
]; ];
module.exports = OutputIndexController; module.exports = OutputIndexController;

View File

@@ -2,244 +2,153 @@
import { OUTPUT_PAGE_LIMIT } from './constants'; import { OUTPUT_PAGE_LIMIT } from './constants';
function PageService ($q) { function PageService ($q) {
this.init = (storage, api, { getScrollHeight }) => { this.init = ({ getPage, getFirst, getLast, getLastPageNumber }) => {
const { prepend, append, shift, pop, deleteRecord } = storage;
const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api;
this.api = { this.api = {
getPage, getPage,
getFirst, getFirst,
getLast, getLast,
getLastPageNumber, getLastPageNumber,
getMaxCounter,
}; };
this.pages = {};
this.storage = { this.state = { head: 0, tail: 0 };
prepend,
append,
shift,
pop,
deleteRecord,
};
this.hooks = {
getScrollHeight,
};
this.records = {};
this.uuids = {};
this.state = {
head: 0,
tail: 0,
};
this.chain = $q.resolve();
};
this.pushFront = (results, key) => {
if (!results) {
return $q.resolve();
}
return this.storage.append(results)
.then(() => {
const tail = key || ++this.state.tail;
this.records[tail] = {};
results.forEach(({ counter, start_line, end_line, uuid }) => {
this.records[tail][counter] = { start_line, end_line };
this.uuids[counter] = uuid;
});
return $q.resolve();
});
};
this.pushBack = (results, key) => {
if (!results) {
return $q.resolve();
}
return this.storage.prepend(results)
.then(() => {
const head = key || --this.state.head;
this.records[head] = {};
results.forEach(({ counter, start_line, end_line, uuid }) => {
this.records[head][counter] = { start_line, end_line };
this.uuids[counter] = uuid;
});
return $q.resolve();
});
};
this.popBack = () => {
if (this.getRecordCount() === 0) {
return $q.resolve();
}
const pageRecord = this.records[this.state.head] || {};
let lines = 0;
const counters = [];
Object.keys(pageRecord)
.forEach(counter => {
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
counters.push(counter);
});
return this.storage.shift(lines)
.then(() => {
counters.forEach(counter => {
this.storage.deleteRecord(this.uuids[counter]);
delete this.uuids[counter];
});
delete this.records[this.state.head++];
return $q.resolve();
});
};
this.popFront = () => {
if (this.getRecordCount() === 0) {
return $q.resolve();
}
const pageRecord = this.records[this.state.tail] || {};
let lines = 0;
const counters = [];
Object.keys(pageRecord)
.forEach(counter => {
lines += pageRecord[counter].end_line - pageRecord[counter].start_line;
counters.push(counter);
});
return this.storage.pop(lines)
.then(() => {
counters.forEach(counter => {
this.storage.deleteRecord(this.uuids[counter]);
delete this.uuids[counter];
});
delete this.records[this.state.tail--];
return $q.resolve();
});
}; };
this.getNext = () => { this.getNext = () => {
const lastPageNumber = this.api.getLastPageNumber(); const lastPageNumber = this.api.getLastPageNumber();
const number = Math.min(this.state.tail + 1, lastPageNumber); const number = Math.min(this.state.tail + 1, lastPageNumber);
const isLoaded = (number >= this.state.head && number <= this.state.tail); if (number < 1) {
const isValid = (number >= 1 && number <= lastPageNumber); return $q.resolve([]);
let popHeight = this.hooks.getScrollHeight();
if (!isValid || isLoaded) {
this.chain = this.chain
.then(() => $q.resolve(popHeight));
return this.chain;
} }
const pageCount = this.state.head - this.state.tail; if (number > lastPageNumber) {
return $q.resolve([]);
if (pageCount >= OUTPUT_PAGE_LIMIT) {
this.chain = this.chain
.then(() => this.popBack())
.then(() => {
popHeight = this.hooks.getScrollHeight();
return $q.resolve();
});
} }
this.chain = this.chain let promise;
.then(() => this.api.getPage(number))
.then(events => this.pushFront(events))
.then(() => $q.resolve(popHeight));
return this.chain; if (this.pages[number]) {
promise = $q.resolve(this.pages[number]);
} else {
promise = this.api.getPage(number);
}
return promise
.then(results => {
if (results.length <= 0) {
return $q.resolve([]);
}
this.state.tail = number;
this.pages[number] = results;
return $q.resolve(results);
});
}; };
this.getPrevious = () => { this.getPrevious = () => {
const number = Math.max(this.state.head - 1, 1); const number = Math.max(this.state.head - 1, 1);
const isLoaded = (number >= this.state.head && number <= this.state.tail); if (number < 1) {
const isValid = (number >= 1 && number <= this.api.getLastPageNumber()); return $q.resolve([]);
let popHeight = this.hooks.getScrollHeight();
if (!isValid || isLoaded) {
this.chain = this.chain
.then(() => $q.resolve(popHeight));
return this.chain;
} }
const pageCount = this.state.head - this.state.tail; if (number > this.api.getLastPageNumber()) {
return $q.resolve([]);
if (pageCount >= OUTPUT_PAGE_LIMIT) {
this.chain = this.chain
.then(() => this.popFront())
.then(() => {
popHeight = this.hooks.getScrollHeight();
return $q.resolve();
});
} }
this.chain = this.chain let promise;
.then(() => this.api.getPage(number))
.then(events => this.pushBack(events))
.then(() => $q.resolve(popHeight));
return this.chain; if (this.pages[number]) {
promise = $q.resolve(this.pages[number]);
} else {
promise = this.api.getPage(number);
}
return promise
.then(results => {
if (results.length <= 0) {
return $q.resolve([]);
}
this.state.head = number;
this.pages[number] = results;
return $q.resolve(results);
});
}; };
this.clear = () => { this.getLast = () => this.api.getLast()
const count = this.getRecordCount(); .then(results => {
if (results.length <= 0) {
return $q.resolve([]);
}
for (let i = 0; i <= count; ++i) { const number = this.api.getLastPageNumber();
this.chain = this.chain.then(() => this.popBack());
}
return this.chain; this.state.head = number;
}; this.state.tail = number;
this.pages[number] = results;
this.getLast = () => this.clear() return $q.resolve(results);
.then(() => this.api.getLast()) });
.then(events => {
const lastPage = this.api.getLastPageNumber();
this.state.head = lastPage; this.getFirst = () => this.api.getFirst()
this.state.tail = lastPage; .then(results => {
if (results.length <= 0) {
return $q.resolve([]);
}
return this.pushBack(events, lastPage);
})
.then(() => this.getPrevious());
this.getFirst = () => this.clear()
.then(() => this.api.getFirst())
.then(events => {
this.state.head = 1; this.state.head = 1;
this.state.tail = 1; this.state.tail = 1;
this.pages[1] = results;
return this.pushBack(events, 1); return $q.resolve(results);
}) });
.then(() => this.getNext());
this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail; this.trimTail = () => {
this.getRecordCount = () => Object.keys(this.records).length; const { tail, head } = this.state;
this.getTailCounter = () => this.state.tail; let popCount = 0;
this.getMaxCounter = () => this.api.getMaxCounter();
for (let i = tail; i > head; i--) {
if (!this.isOverCapacity()) {
break;
}
if (this.pages[i]) {
popCount += this.pages[i].length;
}
delete this.pages[i];
this.state.tail--;
}
return popCount;
};
this.trimHead = () => {
const { head, tail } = this.state;
let popCount = 0;
for (let i = head; i < tail; i++) {
if (!this.isOverCapacity()) {
break;
}
if (this.pages[i]) {
popCount += this.pages[i].length;
}
delete this.pages[i];
this.state.head++;
}
return popCount;
};
this.isOverCapacity = () => this.state.tail - this.state.head > OUTPUT_PAGE_LIMIT;
} }
PageService.$inject = ['$q']; PageService.$inject = ['$q'];

View File

@@ -6,6 +6,7 @@ import {
EVENT_STATS_PLAY, EVENT_STATS_PLAY,
EVENT_START_TASK, EVENT_START_TASK,
OUTPUT_ELEMENT_TBODY, OUTPUT_ELEMENT_TBODY,
OUTPUT_EVENT_LIMIT,
} from './constants'; } from './constants';
const EVENT_GROUPS = [ const EVENT_GROUPS = [
@@ -33,105 +34,235 @@ const hasAnsi = input => re.test(input);
function JobRenderService ($q, $sce, $window) { function JobRenderService ($q, $sce, $window) {
this.init = ({ compile, toggles }) => { this.init = ({ compile, toggles }) => {
this.parent = null;
this.record = {};
this.el = $(OUTPUT_ELEMENT_TBODY);
this.hooks = { compile }; this.hooks = { compile };
this.el = $(OUTPUT_ELEMENT_TBODY);
this.parent = null;
this.createToggles = toggles;
this.state = { this.state = {
collapseAll: false head: 0,
tail: 0,
collapseAll: false,
toggleMode: toggles,
}; };
this.records = {};
this.uuids = {};
}; };
this.setCollapseAll = value => { this.setCollapseAll = value => {
this.state.collapseAll = value; this.state.collapseAll = value;
Object.keys(this.records).forEach(key => {
this.records[key].isCollapsed = value;
});
}; };
this.sortByLineNumber = (a, b) => { this.sortByCounter = (a, b) => {
if (a.start_line > b.start_line) { if (a.counter > b.counter) {
return 1; return 1;
} }
if (a.start_line < b.start_line) { if (a.counter < b.counter) {
return -1; return -1;
} }
return 0; return 0;
}; };
this.transformEventGroup = events => { //
// Event Data Transformation / HTML Building
//
this.appendEventGroup = events => {
let lines = 0; let lines = 0;
let html = ''; let html = '';
events.sort(this.sortByLineNumber); events.sort(this.sortByCounter);
for (let i = 0; i < events.length; ++i) { for (let i = 0; i <= events.length - 1; i++) {
const line = this.transformEvent(events[i]); const current = events[i];
html += line.html;
lines += line.count; if (this.state.tail && current.counter !== this.state.tail + 1) {
const missing = this.appendMissingEventGroup(current);
html += missing.html;
lines += missing.count;
}
const eventLines = this.transformEvent(current);
html += eventLines.html;
lines += eventLines.count;
} }
return { html, lines }; return { html, lines };
}; };
this.transformEvent = event => { this.appendMissingEventGroup = event => {
if (this.record[event.uuid]) { const tailUUID = this.uuids[this.state.tail];
const tailRecord = this.records[tailUUID];
if (!tailRecord) {
return { html: '', count: 0 }; return { html: '', count: 0 };
} }
let uuid;
if (tailRecord.isMissing) {
uuid = tailUUID;
} else {
uuid = `${event.counter}-${tailUUID}`;
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
}
for (let i = this.state.tail + 1; i < event.counter; i++) {
this.records[uuid].counters.push(i);
this.uuids[i] = uuid;
}
if (tailRecord.isMissing) {
return { html: '', count: 0 };
}
if (tailRecord.end === event.start_line) {
return { html: '', count: 0 };
}
const html = this.buildRowHTML(this.records[uuid]);
const count = 1;
return { html, count };
};
this.prependEventGroup = events => {
let lines = 0;
let html = '';
events.sort(this.sortByCounter);
for (let i = events.length - 1; i >= 0; i--) {
const current = events[i];
if (this.state.head && current.counter !== this.state.head - 1) {
const missing = this.prependMissingEventGroup(current);
html = missing.html + html;
lines += missing.count;
}
const eventLines = this.transformEvent(current);
html = eventLines.html + html;
lines += eventLines.count;
}
return { html, lines };
};
this.prependMissingEventGroup = event => {
const headUUID = this.uuids[this.state.head];
const headRecord = this.records[headUUID];
if (!headRecord) {
return { html: '', count: 0 };
}
let uuid;
if (headRecord.isMissing) {
uuid = headUUID;
} else {
uuid = `${headUUID}-${event.counter}`;
this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true };
}
for (let i = this.state.head - 1; i > event.counter; i--) {
this.records[uuid].counters.unshift(i);
this.uuids[i] = uuid;
}
if (headRecord.isMissing) {
return { html: '', count: 0 };
}
if (event.end_line === headRecord.start) {
return { html: '', count: 0 };
}
const html = this.buildRowHTML(this.records[uuid]);
const count = 1;
return { html, count };
};
this.transformEvent = event => {
if (!event || !event.stdout) { if (!event || !event.stdout) {
return { html: '', count: 0 }; return { html: '', count: 0 };
} }
if (event.uuid && this.records[event.uuid]) {
return { html: '', count: 0 };
}
const stdout = this.sanitize(event.stdout); const stdout = this.sanitize(event.stdout);
const lines = stdout.split('\r\n'); const lines = stdout.split('\r\n');
const record = this.createRecord(event, lines);
let html = '';
let count = lines.length; let count = lines.length;
let ln = event.start_line; let ln = event.start_line;
const current = this.createRecord(ln, lines, event); for (let i = 0; i <= lines.length - 1; i++) {
const html = lines.reduce((concat, line, i) => {
ln++; ln++;
const line = lines[i];
const isLastLine = i === lines.length - 1; const isLastLine = i === lines.length - 1;
let row = this.createRow(current, ln, line); let row = this.buildRowHTML(record, ln, line);
if (current && current.isTruncated && isLastLine) { if (record && record.isTruncated && isLastLine) {
row += this.createRow(current); row += this.buildRowHTML(record);
count++; count++;
} }
return `${concat}${row}`; html += row;
}, ''); }
return { html, count }; return { html, count };
}; };
this.isHostEvent = (event) => { this.createRecord = (event, lines) => {
if (typeof event.host === 'number') { if (!event.counter) {
return true;
}
if (event.type === 'project_update_event' &&
event.event !== 'runner_on_skipped' &&
event.event_data.host) {
return true;
}
return false;
};
this.createRecord = (ln, lines, event) => {
if (!event.uuid) {
return null; return null;
} }
const info = { if (!this.state.head || event.counter < this.state.head) {
this.state.head = event.counter;
}
if (!this.state.tail || event.counter > this.state.tail) {
this.state.tail = event.counter;
}
if (!event.uuid) {
this.uuids[event.counter] = event.counter;
this.records[event.counter] = { counters: [event.counter], lineCount: lines.length };
return this.records[event.counter];
}
let isHost = false;
if (typeof event.host === 'number') {
isHost = true;
} else if (event.type === 'project_update_event' &&
event.event !== 'runner_on_skipped' &&
event.event_data.host) {
isHost = true;
}
const record = {
isHost,
id: event.id, id: event.id,
line: ln + 1, line: event.start_line + 1,
name: event.event, name: event.event,
uuid: event.uuid, uuid: event.uuid,
level: event.event_level, level: event.event_level,
@@ -139,54 +270,49 @@ function JobRenderService ($q, $sce, $window) {
end: event.end_line, end: event.end_line,
isTruncated: (event.end_line - event.start_line) > lines.length, isTruncated: (event.end_line - event.start_line) > lines.length,
lineCount: lines.length, lineCount: lines.length,
isHost: this.isHostEvent(event),
isCollapsed: this.state.collapseAll, isCollapsed: this.state.collapseAll,
counters: [event.counter],
}; };
if (event.parent_uuid) { if (event.parent_uuid) {
info.parents = this.getParentEvents(event.parent_uuid); record.parents = this.getParentEvents(event.parent_uuid);
if (this.record[event.parent_uuid]) { if (this.records[event.parent_uuid]) {
info.isCollapsed = this.record[event.parent_uuid].isCollapsed; record.isCollapsed = this.records[event.parent_uuid].isCollapsed;
} }
} }
if (info.isTruncated) { if (record.isTruncated) {
info.truncatedAt = event.start_line + lines.length; record.truncatedAt = event.start_line + lines.length;
} }
if (EVENT_GROUPS.includes(event.event)) { if (EVENT_GROUPS.includes(event.event)) {
info.isParent = true; record.isParent = true;
if (event.event_level === 1) { if (event.event_level === 1) {
this.parent = event.uuid; this.parent = event.uuid;
} }
if (event.parent_uuid) { if (event.parent_uuid) {
if (this.record[event.parent_uuid]) { if (this.records[event.parent_uuid]) {
if (this.record[event.parent_uuid].children && if (this.records[event.parent_uuid].children &&
!this.record[event.parent_uuid].children.includes(event.uuid)) { !this.records[event.parent_uuid].children.includes(event.uuid)) {
this.record[event.parent_uuid].children.push(event.uuid); this.records[event.parent_uuid].children.push(event.uuid);
} else { } else {
this.record[event.parent_uuid].children = [event.uuid]; this.records[event.parent_uuid].children = [event.uuid];
} }
} }
} }
} }
if (TIME_EVENTS.includes(event.event)) { if (TIME_EVENTS.includes(event.event)) {
info.time = this.getTimestamp(event.created); record.time = this.getTimestamp(event.created);
info.line++; record.line++;
} }
this.record[event.uuid] = info; this.records[event.uuid] = record;
this.uuids[event.counter] = event.uuid;
return info; return record;
};
this.getRecord = uuid => this.record[uuid];
this.deleteRecord = uuid => {
delete this.record[uuid];
}; };
this.getParentEvents = (uuid, list) => { this.getParentEvents = (uuid, list) => {
@@ -194,14 +320,14 @@ function JobRenderService ($q, $sce, $window) {
// always push its parent if exists // always push its parent if exists
list.push(uuid); list.push(uuid);
// if we can get grandparent in current visible lines, we also push it // if we can get grandparent in current visible lines, we also push it
if (this.record[uuid] && this.record[uuid].parents) { if (this.records[uuid] && this.records[uuid].parents) {
list = list.concat(this.record[uuid].parents); list = list.concat(this.records[uuid].parents);
} }
return list; return list;
}; };
this.createRow = (current, ln, content) => { this.buildRowHTML = (record, ln, content) => {
let id = ''; let id = '';
let icon = ''; let icon = '';
let timestamp = ''; let timestamp = '';
@@ -209,17 +335,23 @@ function JobRenderService ($q, $sce, $window) {
let tdEvent = ''; let tdEvent = '';
let classList = ''; let classList = '';
if (record.isMissing) {
return `<div id="${record.uuid}" class="at-Stdout-row">
<div class="at-Stdout-toggle"></div>
<div class="at-Stdout-line at-Stdout-line--clickable" ng-click="vm.showMissingEvents('${record.uuid}')">...</div></div>`;
}
content = content || ''; content = content || '';
if (hasAnsi(content)) { if (hasAnsi(content)) {
content = ansi.toHtml(content); content = ansi.toHtml(content);
} }
if (current) { if (record) {
if (this.createToggles && current.isParent && current.line === ln) { if (this.state.toggleMode && record.isParent && record.line === ln) {
id = current.uuid; id = record.uuid;
if (current.isCollapsed) { if (record.isCollapsed) {
icon = 'fa-angle-right'; icon = 'fa-angle-right';
} else { } else {
icon = 'fa-angle-down'; icon = 'fa-angle-down';
@@ -228,16 +360,16 @@ function JobRenderService ($q, $sce, $window) {
tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleCollapse('${id}')"><i class="fa ${icon} can-toggle"></i></div>`; tdToggle = `<div class="at-Stdout-toggle" ng-click="vm.toggleCollapse('${id}')"><i class="fa ${icon} can-toggle"></i></div>`;
} }
if (current.isHost) { if (record.isHost) {
tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}', '${current.uuid}')"><span ng-non-bindable>${content}</span></div>`; tdEvent = `<div class="at-Stdout-event--host" ng-click="vm.showHostDetails('${record.id}', '${record.uuid}')"><span ng-non-bindable>${content}</span></div>`;
} }
if (current.time && current.line === ln) { if (record.time && record.line === ln) {
timestamp = `<span>${current.time}</span>`; timestamp = `<span>${record.time}</span>`;
} }
if (current.parents) { if (record.parents) {
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); classList = record.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
} }
} }
@@ -253,8 +385,8 @@ function JobRenderService ($q, $sce, $window) {
ln = '...'; ln = '...';
} }
if (current && current.isCollapsed) { if (record && record.isCollapsed) {
if (current.level === 3 || current.level === 0) { if (record.level === 3 || record.level === 0) {
classList += ' hidden'; classList += ' hidden';
} }
} }
@@ -277,6 +409,10 @@ function JobRenderService ($q, $sce, $window) {
return `${hour}:${minute}:${second}`; return `${hour}:${minute}:${second}`;
}; };
//
// Element Operations
//
this.remove = elements => this.requestAnimationFrame(() => elements.remove()); this.remove = elements => this.requestAnimationFrame(() => elements.remove());
this.requestAnimationFrame = fn => $q(resolve => { this.requestAnimationFrame = fn => $q(resolve => {
@@ -295,7 +431,7 @@ function JobRenderService ($q, $sce, $window) {
return this.requestAnimationFrame(); return this.requestAnimationFrame();
}; };
this.clear = () => { this.removeAll = () => {
const elements = this.el.children(); const elements = this.el.children();
return this.remove(elements); return this.remove(elements);
}; };
@@ -317,7 +453,7 @@ function JobRenderService ($q, $sce, $window) {
return $q.resolve(); return $q.resolve();
} }
const result = this.transformEventGroup(events); const result = this.prependEventGroup(events);
const html = this.trustHtml(result.html); const html = this.trustHtml(result.html);
const newElements = angular.element(html); const newElements = angular.element(html);
@@ -332,7 +468,7 @@ function JobRenderService ($q, $sce, $window) {
return $q.resolve(); return $q.resolve();
} }
const result = this.transformEventGroup(events); const result = this.appendEventGroup(events);
const html = this.trustHtml(result.html); const html = this.trustHtml(result.html);
const newElements = angular.element(html); const newElements = angular.element(html);
@@ -343,8 +479,110 @@ function JobRenderService ($q, $sce, $window) {
}; };
this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html)); this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html));
this.sanitize = html => entities.encode(html); this.sanitize = html => entities.encode(html);
//
// Event Counter Methods - External code should prefer these.
//
this.clear = () => this.removeAll()
.then(() => {
const head = this.getHeadCounter();
const tail = this.getTailCounter();
for (let i = head; i <= tail; ++i) {
const uuid = this.uuids[i];
if (uuid) {
delete this.records[uuid];
delete this.uuids[i];
}
}
this.state.head = 0;
this.state.tail = 0;
return $q.resolve();
});
this.pushFront = events => {
const tail = this.getTailCounter();
return this.append(events.filter(({ counter }) => counter > tail));
};
this.pushBack = events => {
const head = this.getHeadCounter();
const tail = this.getTailCounter();
return this.prepend(events.filter(({ counter }) => counter < head || counter > tail));
};
this.popFront = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const max = this.state.tail;
const min = max - count;
let lines = 0;
for (let i = max; i >= min; --i) {
const uuid = this.uuids[i];
if (!uuid) {
continue;
}
this.records[uuid].counters.pop();
delete this.uuids[i];
if (this.records[uuid].counters.length === 0) {
lines += this.records[uuid].lineCount;
delete this.records[uuid];
this.state.tail--;
}
}
return this.pop(lines);
};
this.popBack = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const min = this.state.head;
const max = min + count;
let lines = 0;
for (let i = min; i <= max; ++i) {
const uuid = this.uuids[i];
if (!uuid) {
continue;
}
this.records[uuid].counters.shift();
delete this.uuids[i];
if (this.records[uuid].counters.length === 0) {
lines += this.records[uuid].lineCount;
delete this.records[uuid];
this.state.head++;
}
}
return this.shift(lines);
};
this.getHeadCounter = () => this.state.head;
this.getTailCounter = () => this.state.tail;
this.getCapacity = () => OUTPUT_EVENT_LIMIT - (this.getTailCounter() - this.getHeadCounter());
} }
JobRenderService.$inject = ['$q', '$sce', '$window']; JobRenderService.$inject = ['$q', '$sce', '$window'];

View File

@@ -1,42 +1,12 @@
/* eslint camelcase: 0 */ /* eslint camelcase: 0 */
import { import {
API_MAX_PAGE_SIZE, OUTPUT_MAX_BUFFER_LENGTH,
OUTPUT_EVENT_LIMIT,
OUTPUT_PAGE_SIZE, OUTPUT_PAGE_SIZE,
} from './constants'; } from './constants';
function getContinuous (events, reverse = false) {
const counters = events.map(({ counter }) => counter);
const min = Math.min(...counters);
const max = Math.max(...counters);
const missing = [];
for (let i = min; i <= max; i++) {
if (counters.indexOf(i) < 0) {
missing.push(i);
}
}
if (missing.length === 0) {
return events;
}
if (reverse) {
const threshold = Math.max(...missing);
return events.filter(({ counter }) => counter > threshold);
}
const threshold = Math.min(...missing);
return events.filter(({ counter }) => counter < threshold);
}
function SlidingWindowService ($q) { function SlidingWindowService ($q) {
this.init = (storage, api, { getScrollHeight }) => { this.init = ({ getRange, getFirst, getLast, getMaxCounter }, storage) => {
const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage; const { getHeadCounter, getTailCounter } = storage;
const { getRange, getFirst, getLast, getMaxCounter } = api;
this.api = { this.api = {
getRange, getRange,
@@ -46,32 +16,20 @@ function SlidingWindowService ($q) {
}; };
this.storage = { this.storage = {
clear, getHeadCounter,
prepend, getTailCounter,
append,
shift,
pop,
getRecord,
deleteRecord,
}; };
this.hooks = {
getScrollHeight,
};
this.lines = {};
this.uuids = {};
this.chain = $q.resolve();
this.state = { head: null, tail: null };
this.cache = { first: null };
this.buffer = { this.buffer = {
events: [], events: [],
min: 0, min: 0,
max: 0, max: 0,
count: 0, count: 0,
}; };
this.cache = {
first: null
};
}; };
this.getBoundedRange = range => { this.getBoundedRange = range => {
@@ -92,273 +50,46 @@ function SlidingWindowService ($q) {
return this.getBoundedRange([head - 1 - displacement, head - 1]); return this.getBoundedRange([head - 1 - displacement, head - 1]);
}; };
this.createRecord = ({ counter, uuid, start_line, end_line }) => {
this.lines[counter] = end_line - start_line;
this.uuids[counter] = uuid;
if (this.state.tail === null) {
this.state.tail = counter;
}
if (counter > this.state.tail) {
this.state.tail = counter;
}
if (this.state.head === null) {
this.state.head = counter;
}
if (counter < this.state.head) {
this.state.head = counter;
}
};
this.deleteRecord = counter => {
this.storage.deleteRecord(this.uuids[counter]);
delete this.uuids[counter];
delete this.lines[counter];
};
this.getLineCount = counter => {
const record = this.storage.getRecord(counter);
if (record && record.lineCount) {
return record.lineCount;
}
if (this.lines[counter]) {
return this.lines[counter];
}
return 0;
};
this.pushFront = events => {
const tail = this.getTailCounter();
const newEvents = events.filter(({ counter }) => counter > tail);
return this.storage.append(newEvents)
.then(() => {
newEvents.forEach(event => this.createRecord(event));
return $q.resolve();
});
};
this.pushBack = events => {
const [head, tail] = this.getRange();
const newEvents = events
.filter(({ counter }) => counter < head || counter > tail);
return this.storage.prepend(newEvents)
.then(() => {
newEvents.forEach(event => this.createRecord(event));
return $q.resolve();
});
};
this.popFront = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const max = this.getTailCounter();
const min = max - count;
let lines = 0;
for (let i = max; i >= min; --i) {
lines += this.getLineCount(i);
}
return this.storage.pop(lines)
.then(() => {
for (let i = max; i >= min; --i) {
this.deleteRecord(i);
this.state.tail--;
}
return $q.resolve();
});
};
this.popBack = count => {
if (!count || count <= 0) {
return $q.resolve();
}
const min = this.getHeadCounter();
const max = min + count;
let lines = 0;
for (let i = min; i <= max; ++i) {
lines += this.getLineCount(i);
}
return this.storage.shift(lines)
.then(() => {
for (let i = min; i <= max; ++i) {
this.deleteRecord(i);
this.state.head++;
}
return $q.resolve();
});
};
this.clear = () => this.storage.clear()
.then(() => {
const [head, tail] = this.getRange();
for (let i = head; i <= tail; ++i) {
this.deleteRecord(i);
}
this.state.head = null;
this.state.tail = null;
return $q.resolve();
});
this.getNext = (displacement = OUTPUT_PAGE_SIZE) => { this.getNext = (displacement = OUTPUT_PAGE_SIZE) => {
const next = this.getNextRange(displacement); const next = this.getNextRange(displacement);
const [head, tail] = this.getRange();
this.chain = this.chain return this.api.getRange(next);
.then(() => this.api.getRange(next))
.then(events => {
const results = getContinuous(events);
const min = Math.min(...results.map(({ counter }) => counter));
if (min > tail + 1) {
return $q.resolve([]);
}
return $q.resolve(results);
})
.then(results => {
const count = (tail - head + results.length);
const excess = count - OUTPUT_EVENT_LIMIT;
return this.popBack(excess)
.then(() => {
const popHeight = this.hooks.getScrollHeight();
return this.pushFront(results).then(() => $q.resolve(popHeight));
});
});
return this.chain;
}; };
this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => { this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => {
const previous = this.getPreviousRange(displacement); const previous = this.getPreviousRange(displacement);
const [head, tail] = this.getRange();
this.chain = this.chain return this.api.getRange(previous);
.then(() => this.api.getRange(previous))
.then(events => {
const results = getContinuous(events, true);
const max = Math.max(...results.map(({ counter }) => counter));
if (head > max + 1) {
return $q.resolve([]);
}
return $q.resolve(results);
})
.then(results => {
const count = (tail - head + results.length);
const excess = count - OUTPUT_EVENT_LIMIT;
return this.popFront(excess)
.then(() => {
const popHeight = this.hooks.getScrollHeight();
return this.pushBack(results).then(() => $q.resolve(popHeight));
});
});
return this.chain;
}; };
this.getFirst = () => { this.getFirst = () => {
this.chain = this.chain if (this.cache.first) {
.then(() => this.clear()) return $q.resolve(this.cache.first);
.then(() => { }
if (this.cache.first) {
return $q.resolve(this.cache.first);
}
return this.api.getFirst(); return this.api.getFirst()
})
.then(events => { .then(events => {
if (events.length === OUTPUT_PAGE_SIZE) { if (events.length === OUTPUT_PAGE_SIZE) {
this.cache.first = events; this.cache.first = events;
} }
return this.pushFront(events); return $q.resolve(events);
}); });
return this.chain
.then(() => this.getNext());
}; };
this.getLast = () => { this.getLast = () => this.getFrames()
this.chain = this.chain .then(frames => {
.then(() => this.getFrames()) if (frames.length > 0) {
.then(frames => { return $q.resolve(frames);
if (frames.length > 0) { }
return $q.resolve(frames);
}
return this.api.getLast(); return this.api.getLast();
}) });
.then(events => {
const min = Math.min(...events.map(({ counter }) => counter));
if (min <= this.getTailCounter() + 1) {
return this.pushFront(events);
}
return this.clear()
.then(() => this.pushBack(events));
});
return this.chain
.then(() => this.getPrevious());
};
this.getTailCounter = () => {
if (this.state.tail === null) {
return 0;
}
if (this.state.tail < 0) {
return 0;
}
return this.state.tail;
};
this.getHeadCounter = () => {
if (this.state.head === null) {
return 0;
}
if (this.state.head < 0) {
return 0;
}
return this.state.head;
};
this.pushFrames = events => { this.pushFrames = events => {
const head = this.getHeadCounter();
const tail = this.getTailCounter();
const frames = this.buffer.events.concat(events); const frames = this.buffer.events.concat(events);
const [head, tail] = this.getRange();
let min; let min;
let max; let max;
@@ -367,7 +98,7 @@ function SlidingWindowService ($q) {
for (let i = frames.length - 1; i >= 0; i--) { for (let i = frames.length - 1; i >= 0; i--) {
count++; count++;
if (count > API_MAX_PAGE_SIZE) { if (count > OUTPUT_MAX_BUFFER_LENGTH) {
frames.splice(i, 1); frames.splice(i, 1);
count--; count--;
@@ -388,27 +119,34 @@ function SlidingWindowService ($q) {
this.buffer.max = max; this.buffer.max = max;
this.buffer.count = count; this.buffer.count = count;
if (min >= head && min <= tail + 1) { if (tail - head === 0) {
return frames.filter(({ counter }) => counter > tail); return frames;
} }
return []; return frames.filter(({ counter }) => counter > tail);
}; };
this.getFrames = () => $q.resolve(this.buffer.events); this.getFrames = () => $q.resolve(this.buffer.events);
this.getMaxCounter = () => { this.getMaxCounter = () => {
if (this.buffer.min) { if (this.buffer.max && this.buffer.max > 1) {
return this.buffer.min; return this.buffer.max;
} }
return this.api.getMaxCounter(); return this.api.getMaxCounter();
}; };
this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); this.isOnLastPage = () => {
this.getRange = () => [this.getHeadCounter(), this.getTailCounter()]; if (this.buffer.min) {
this.getRecordCount = () => Object.keys(this.lines).length; return this.getTailCounter() >= this.buffer.min - 1;
this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount(); }
return this.getTailCounter() >= this.getMaxCounter() - OUTPUT_PAGE_SIZE;
};
this.isOnFirstPage = () => this.getHeadCounter() === 1;
this.getTailCounter = () => this.storage.getTailCounter();
this.getHeadCounter = () => this.storage.getHeadCounter();
} }
SlidingWindowService.$inject = ['$q']; SlidingWindowService.$inject = ['$q'];

View File

@@ -1,37 +1,49 @@
/* eslint camelcase: 0 */ /* eslint camelcase: 0 */
import { import {
EVENT_STATS_PLAY, EVENT_STATS_PLAY,
OUTPUT_MAX_BUFFER_LENGTH,
OUTPUT_MAX_LAG, OUTPUT_MAX_LAG,
OUTPUT_PAGE_SIZE, OUTPUT_PAGE_SIZE,
OUTPUT_EVENT_LIMIT,
} from './constants'; } from './constants';
const rx = [];
function OutputStream ($q) { function OutputStream ($q) {
this.init = ({ bufferAdd, bufferEmpty, onFrames, onFrameRate, onStop }) => { this.init = ({ onFrames, onFrameRate, onStop }) => {
this.hooks = { this.hooks = {
bufferAdd,
bufferEmpty,
onFrames, onFrames,
onFrameRate, onFrameRate,
onStop, onStop,
}; };
this.bufferInit();
};
this.bufferInit = () => {
rx.length = 0;
this.counters = { this.counters = {
used: [],
ready: [],
min: 1, min: 1,
max: 0, max: -1,
ready: -1,
final: null, final: null,
used: [],
missing: [],
total: 0,
length: 0,
}; };
this.state = { this.state = {
ending: false, ending: false,
ended: false, ended: false,
overflow: false,
}; };
this.lag = 0; this.lag = 0;
this.chain = $q.resolve(); this.chain = $q.resolve();
this.factors = this.calcFactors(OUTPUT_PAGE_SIZE); this.factors = this.calcFactors(OUTPUT_EVENT_LIMIT);
this.setFramesPerRender(); this.setFramesPerRender();
}; };
@@ -63,36 +75,87 @@ function OutputStream ($q) {
} }
}; };
this.updateCounterState = ({ counter }) => { this.bufferAdd = event => {
this.counters.used.push(counter); const { counter } = event;
if (counter > this.counters.max) { if (counter > this.counters.max) {
this.counters.max = counter; this.counters.max = counter;
} }
let ready;
const used = [];
const missing = []; const missing = [];
let minReady;
let maxReady;
for (let i = this.counters.min; i <= this.counters.max; i++) { for (let i = this.counters.min; i <= this.counters.max; i++) {
if (this.counters.used.indexOf(i) === -1) { if (this.counters.used.indexOf(i) === -1) {
missing.push(i); if (i === counter) {
} else if (missing.length === 0) { rx.push(event);
maxReady = i; used.push(i);
this.counters.length += 1;
} else {
missing.push(i);
}
} else {
used.push(i);
} }
} }
if (maxReady) { const excess = this.counters.length - OUTPUT_MAX_BUFFER_LENGTH;
minReady = this.counters.min; this.state.overflow = (excess > 0);
this.counters.min = maxReady + 1; if (missing.length === 0) {
this.counters.used = this.counters.used.filter(c => c > maxReady); ready = this.counters.max;
} else if (this.state.overflow) {
ready = this.counters.min + this.framesPerRender;
} else {
ready = missing[0] - 1;
} }
this.counters.total += 1;
this.counters.ready = ready;
this.counters.used = used;
this.counters.missing = missing; this.counters.missing = missing;
this.counters.ready = [minReady, maxReady]; };
return this.counters.ready; this.bufferEmpty = threshold => {
let removed = [];
for (let i = rx.length - 1; i >= 0; i--) {
if (rx[i].counter <= threshold) {
removed = removed.concat(rx.splice(i, 1));
}
}
this.counters.min = threshold + 1;
this.counters.used = this.counters.used.filter(c => c > threshold);
this.counters.length = rx.length;
return removed;
};
this.isReadyToRender = () => {
const { total } = this.counters;
const readyCount = this.counters.ready - this.counters.min;
if (readyCount <= 0) {
return false;
}
if (this.state.ending) {
return true;
}
if (total % this.framesPerRender === 0) {
return true;
}
if (total < OUTPUT_PAGE_SIZE) {
if (readyCount % this.framesPerRender === 0) {
return true;
}
}
return false;
}; };
this.pushJobEvent = data => { this.pushJobEvent = data => {
@@ -105,25 +168,24 @@ function OutputStream ($q) {
this.counters.final = data.counter; this.counters.final = data.counter;
} }
const [minReady, maxReady] = this.updateCounterState(data); this.bufferAdd(data);
const count = this.hooks.bufferAdd(data);
if (count % OUTPUT_PAGE_SIZE === 0) { if (this.counters.total % OUTPUT_PAGE_SIZE === 0) {
this.setFramesPerRender(); this.setFramesPerRender();
} }
const isReady = maxReady && (this.state.ending || if (!this.isReadyToRender()) {
count % this.framesPerRender === 0 ||
count < OUTPUT_PAGE_SIZE && (maxReady - minReady) % this.framesPerRender === 0);
if (!isReady) {
return $q.resolve(); return $q.resolve();
} }
const isLastFrame = this.state.ending && (maxReady >= this.counters.final); const isLast = this.state.ending && (this.counters.ready >= this.counters.final);
const events = this.hooks.bufferEmpty(minReady, maxReady); const events = this.bufferEmpty(this.counters.ready);
return this.emitFrames(events, isLastFrame); if (events.length > 0) {
return this.emitFrames(events, isLast);
}
return $q.resolve();
}) })
.then(() => --this.lag); .then(() => --this.lag);
@@ -136,16 +198,20 @@ function OutputStream ($q) {
this.state.ending = true; this.state.ending = true;
this.counters.final = counter; this.counters.final = counter;
if (counter >= this.counters.min) { if (counter > this.counters.ready) {
return $q.resolve(); return $q.resolve();
} }
const readyCount = this.counters.ready - this.counters.min;
let events = []; let events = [];
if (this.counters.ready.length > 0) { if (readyCount > 0) {
events = this.hooks.bufferEmpty(...this.counters.ready); events = this.bufferEmpty(this.counters.ready);
return this.emitFrames(events, true);
} }
return this.emitFrames(events, true); return $q.resolve();
}); });
return this.chain; return this.chain;
@@ -160,7 +226,6 @@ function OutputStream ($q) {
this.hooks.onStop(); this.hooks.onStop();
} }
this.counters.ready.length = 0;
return $q.resolve(); return $q.resolve();
}); });