From 2187655c6847c0afa3271f91d42d4bd9befebbd1 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sat, 18 Aug 2018 11:50:04 -0400 Subject: [PATCH 01/10] move buffer mgmt to stream service --- .../features/output/index.controller.js | 44 +--------- .../client/features/output/stream.service.js | 81 ++++++++++++------- 2 files changed, 56 insertions(+), 69 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 18e0357ed1..1049b865da 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -20,50 +20,16 @@ let stream; let vm; -const bufferState = [0, 0]; // [length, count] const listeners = []; -const rx = []; - -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) { + events = slide.pushFrames(events); + if (lockFrames) { - events.forEach(bufferAdd); return $q.resolve(); } - events = slide.pushFrames(events); const popCount = events.length - slide.getCapacity(); const isAttached = events.length > 0; @@ -481,7 +447,7 @@ function clear () { lockFollow = false; lockFrames = false; - bufferInit(); + stream.bufferInit(); status.init(resource); slide.init(render, resource.events, scroll); status.subscribe(data => { vm.status = data.status; }); @@ -543,8 +509,6 @@ function OutputIndexController ( vm.debug = _debug; render.requestAnimationFrame(() => { - bufferInit(); - status.init(resource); slide.init(render, resource.events, scroll); render.init({ compile, toggles: vm.toggleLineEnabled }); @@ -564,8 +528,6 @@ function OutputIndexController ( let showFollowTip = true; const rates = []; stream.init({ - bufferAdd, - bufferEmpty, onFrames, onFrameRate (rate) { rates.push(rate); diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index 6d3f2a4137..efdefead54 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -5,24 +5,16 @@ import { OUTPUT_PAGE_SIZE, } from './constants'; +const rx = []; + function OutputStream ($q) { - this.init = ({ bufferAdd, bufferEmpty, onFrames, onFrameRate, onStop }) => { + this.init = ({ onFrames, onFrameRate, onStop }) => { this.hooks = { - bufferAdd, - bufferEmpty, onFrames, onFrameRate, onStop, }; - this.counters = { - used: [], - ready: [], - min: 1, - max: 0, - final: null, - }; - this.state = { ending: false, ended: false, @@ -30,9 +22,44 @@ function OutputStream ($q) { this.lag = 0; this.chain = $q.resolve(); - this.factors = this.calcFactors(OUTPUT_PAGE_SIZE); + this.setFramesPerRender(); + this.bufferInit(); + }; + + this.bufferInit = () => { + rx.length = 0; + + this.counters = { + total: 0, + min: 0, + max: null, + final: null, + ready: [], + used: [], + missing: [], + }; + }; + + this.bufferEmpty = (minReady, maxReady) => { + let removed = []; + + for (let i = rx.length - 1; i >= 0; i--) { + if (rx[i].counter <= maxReady) { + removed = removed.concat(rx.splice(i, 1)); + } + } + + return removed; + }; + + this.bufferAdd = event => { + rx.push(event); + + this.counters.total += 1; + + return this.counters.total; }; this.calcFactors = size => { @@ -63,34 +90,32 @@ function OutputStream ($q) { } }; - this.updateCounterState = ({ counter }) => { + this.checkCounter = ({ counter }) => { this.counters.used.push(counter); - if (counter > this.counters.max) { + if (!this.counters.max || this.counters.max < counter) { this.counters.max = counter; } + let ready; const missing = []; - let minReady; - let maxReady; for (let i = this.counters.min; i <= this.counters.max; i++) { if (this.counters.used.indexOf(i) === -1) { missing.push(i); - } else if (missing.length === 0) { - maxReady = i; } } - if (maxReady) { - minReady = this.counters.min; - - this.counters.min = maxReady + 1; - this.counters.used = this.counters.used.filter(c => c > maxReady); + if (missing.length === 0) { + ready = this.counters.max; + } else { + ready = missing[0] - 1; } + this.counters.ready = [this.counters.min, ready]; + this.counters.min = ready + 1; + this.counters.used = this.counters.used.filter(c => c > ready); this.counters.missing = missing; - this.counters.ready = [minReady, maxReady]; return this.counters.ready; }; @@ -105,8 +130,8 @@ function OutputStream ($q) { this.counters.final = data.counter; } - const [minReady, maxReady] = this.updateCounterState(data); - const count = this.hooks.bufferAdd(data); + const [minReady, maxReady] = this.checkCounter(data); + const count = this.bufferAdd(data); if (count % OUTPUT_PAGE_SIZE === 0) { this.setFramesPerRender(); @@ -121,7 +146,7 @@ function OutputStream ($q) { } const isLastFrame = this.state.ending && (maxReady >= this.counters.final); - const events = this.hooks.bufferEmpty(minReady, maxReady); + const events = this.bufferEmpty(minReady, maxReady); return this.emitFrames(events, isLastFrame); }) @@ -142,7 +167,7 @@ function OutputStream ($q) { let events = []; if (this.counters.ready.length > 0) { - events = this.hooks.bufferEmpty(...this.counters.ready); + events = this.bufferEmpty(...this.counters.ready); } return this.emitFrames(events, true); From 38b9b47e6bed40e5c7ede6aa8a9afcb80ebf5b03 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sat, 18 Aug 2018 23:34:01 -0400 Subject: [PATCH 02/10] add max event count and discarding to stream service --- awx/ui/client/features/output/constants.js | 1 + .../features/output/index.controller.js | 8 +- .../client/features/output/render.service.js | 23 ++- .../client/features/output/slide.service.js | 19 ++- .../client/features/output/stream.service.js | 150 +++++++++++------- 5 files changed, 137 insertions(+), 64 deletions(-) diff --git a/awx/ui/client/features/output/constants.js b/awx/ui/client/features/output/constants.js index a3acc997b6..9f3b18c8cb 100644 --- a/awx/ui/client/features/output/constants.js +++ b/awx/ui/client/features/output/constants.js @@ -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_TBODY = '#atStdoutResultTable'; export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast'; +export const OUTPUT_MAX_BUFFER_LENGTH = 500; export const OUTPUT_MAX_LAG = 120; export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update']; export const OUTPUT_ORDER_BY = 'counter'; diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 1049b865da..0f0579e14b 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -177,8 +177,7 @@ function canStartFollowing () { if (followOnce && // one-time activation from top of first page scroll.isBeyondUpperThreshold() && - slide.getHeadCounter() === 1 && - slide.getTailCounter() >= OUTPUT_PAGE_SIZE) { + slide.getTailCounter() - slide.getHeadCounter() >= OUTPUT_PAGE_SIZE) { followOnce = false; return true; @@ -386,7 +385,7 @@ function startListening () { function handleJobEvent (data) { 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 => { results.push(data); @@ -406,12 +405,13 @@ function handleJobEvent (data) { results = results.filter(({ counter }) => counter > maxMissing); } - stream.setMissingCounterThreshold(max); results.forEach(item => { stream.pushJobEvent(item); status.pushJobEvent(item); }); + stream.setMissingCounterThreshold(min); + return $q.resolve(); }); diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 2c8e296a31..b9b925f2cc 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -31,6 +31,8 @@ const pattern = [ const re = new RegExp(pattern); const hasAnsi = input => re.test(input); +const MISSING_EVENT_GROUP = 'MISSING_EVENT_GROUP'; + function JobRenderService ($q, $sce, $window) { this.init = ({ compile, toggles }) => { this.parent = null; @@ -39,6 +41,7 @@ function JobRenderService ($q, $sce, $window) { this.hooks = { compile }; this.createToggles = toggles; + this.lastMissing = false; this.state = { collapseAll: false }; @@ -76,10 +79,21 @@ function JobRenderService ($q, $sce, $window) { }; this.transformEvent = event => { - if (this.record[event.uuid]) { + if (event.uuid && this.record[event.uuid]) { return { html: '', count: 0 }; } + if (event.event === MISSING_EVENT_GROUP) { + if (this.lastMissing) { + return { html: '', count: 0 }; + } + + this.lastMissing = true; + return this.transformMissingEvent(event); + } + + this.lastMissing = false; + if (!event || !event.stdout) { return { html: '', count: 0 }; } @@ -110,6 +124,13 @@ function JobRenderService ($q, $sce, $window) { return { html, count }; }; + this.transformMissingEvent = () => { + const html = '
...
'; + const count = 1; + + return { html, count }; + }; + this.isHostEvent = (event) => { if (typeof event.host === 'number') { return true; diff --git a/awx/ui/client/features/output/slide.service.js b/awx/ui/client/features/output/slide.service.js index 8bddc51565..df6105617e 100644 --- a/awx/ui/client/features/output/slide.service.js +++ b/awx/ui/client/features/output/slide.service.js @@ -75,7 +75,7 @@ function SlidingWindowService ($q) { }; this.getBoundedRange = range => { - const bounds = [1, this.getMaxCounter()]; + const bounds = [0, this.getMaxCounter()]; return [Math.max(range[0], bounds[0]), Math.min(range[1], bounds[1])]; }; @@ -388,6 +388,10 @@ function SlidingWindowService ($q) { this.buffer.max = max; this.buffer.count = count; + if (tail - head === 0) { + return frames; + } + if (min >= head && min <= tail + 1) { return frames.filter(({ counter }) => counter > tail); } @@ -398,14 +402,21 @@ function SlidingWindowService ($q) { this.getFrames = () => $q.resolve(this.buffer.events); this.getMaxCounter = () => { - if (this.buffer.min) { - return this.buffer.min; + if (this.buffer.min && this.buffer.min > 1) { + return this.buffer.min - 1; } return this.api.getMaxCounter(); }; - this.isOnLastPage = () => this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); + this.isOnLastPage = () => { + if (this.getTailCounter() === 0) { + return true; + } + + return this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); + }; + this.getRange = () => [this.getHeadCounter(), this.getTailCounter()]; this.getRecordCount = () => Object.keys(this.lines).length; this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount(); diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index efdefead54..dc874ca0b9 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -1,6 +1,7 @@ /* eslint camelcase: 0 */ import { EVENT_STATS_PLAY, + OUTPUT_MAX_BUFFER_LENGTH, OUTPUT_MAX_LAG, OUTPUT_PAGE_SIZE, } from './constants'; @@ -15,16 +16,6 @@ function OutputStream ($q) { onStop, }; - this.state = { - ending: false, - ended: false, - }; - - this.lag = 0; - this.chain = $q.resolve(); - this.factors = this.calcFactors(OUTPUT_PAGE_SIZE); - - this.setFramesPerRender(); this.bufferInit(); }; @@ -32,34 +23,27 @@ function OutputStream ($q) { rx.length = 0; this.counters = { - total: 0, - min: 0, - max: null, + min: 1, + max: -1, + ready: -1, final: null, - ready: [], used: [], missing: [], + total: 0, + length: 0, }; - }; - this.bufferEmpty = (minReady, maxReady) => { - let removed = []; + this.state = { + ending: false, + ended: false, + overflow: false, + }; - for (let i = rx.length - 1; i >= 0; i--) { - if (rx[i].counter <= maxReady) { - removed = removed.concat(rx.splice(i, 1)); - } - } + this.lag = 0; + this.chain = $q.resolve(); - return removed; - }; - - this.bufferAdd = event => { - rx.push(event); - - this.counters.total += 1; - - return this.counters.total; + this.factors = this.calcFactors(OUTPUT_PAGE_SIZE); + this.setFramesPerRender(); }; this.calcFactors = size => { @@ -90,34 +74,88 @@ function OutputStream ($q) { } }; - this.checkCounter = ({ counter }) => { - this.counters.used.push(counter); + this.bufferAdd = event => { + const { counter } = event; - if (!this.counters.max || this.counters.max < counter) { + if (counter > this.counters.max) { this.counters.max = counter; } let ready; + const used = []; const missing = []; for (let i = this.counters.min; i <= this.counters.max; i++) { if (this.counters.used.indexOf(i) === -1) { - missing.push(i); + if (i === counter) { + rx.push(event); + used.push(i); + this.counters.length += 1; + } else { + missing.push(i); + } + } else { + used.push(i); } } + const excess = this.counters.length - OUTPUT_MAX_BUFFER_LENGTH; + this.state.overflow = (excess > 0); + if (missing.length === 0) { ready = this.counters.max; + } else if (this.state.overflow) { + const removed = Math.min(missing.length, excess); + ready = missing[removed - 1] - 1; } else { ready = missing[0] - 1; } - this.counters.ready = [this.counters.min, ready]; - this.counters.min = ready + 1; - this.counters.used = this.counters.used.filter(c => c > ready); + this.counters.total += 1; + this.counters.ready = ready; + this.counters.used = used; this.counters.missing = missing; + }; - 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 => { @@ -130,25 +168,24 @@ function OutputStream ($q) { this.counters.final = data.counter; } - const [minReady, maxReady] = this.checkCounter(data); - const count = this.bufferAdd(data); + this.bufferAdd(data); - if (count % OUTPUT_PAGE_SIZE === 0) { + if (this.counters.total % OUTPUT_PAGE_SIZE === 0) { this.setFramesPerRender(); } - const isReady = maxReady && (this.state.ending || - count % this.framesPerRender === 0 || - count < OUTPUT_PAGE_SIZE && (maxReady - minReady) % this.framesPerRender === 0); - - if (!isReady) { + if (!this.isReadyToRender()) { return $q.resolve(); } - const isLastFrame = this.state.ending && (maxReady >= this.counters.final); - const events = this.bufferEmpty(minReady, maxReady); + const isLast = this.state.ending && (this.counters.ready >= this.counters.final); + 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); @@ -161,16 +198,20 @@ function OutputStream ($q) { this.state.ending = true; this.counters.final = counter; - if (counter >= this.counters.min) { + if (counter > this.counters.ready) { return $q.resolve(); } + const readyCount = this.counters.ready - this.counters.min; + let events = []; - if (this.counters.ready.length > 0) { - events = this.bufferEmpty(...this.counters.ready); + if (readyCount > 0) { + events = this.bufferEmpty(this.counters.ready); + + return this.emitFrames(events, true); } - return this.emitFrames(events, true); + return $q.resolve(); }); return this.chain; @@ -185,7 +226,6 @@ function OutputStream ($q) { this.hooks.onStop(); } - this.counters.ready.length = 0; return $q.resolve(); }); From ee348b7169d63a45c9491040c3c3bb8e2850f0ed Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 19 Aug 2018 21:54:34 -0400 Subject: [PATCH 03/10] add handling for discontinuities in render service --- awx/ui/client/features/output/_index.less | 4 + .../client/features/output/render.service.js | 444 ++++++++++++++---- 2 files changed, 348 insertions(+), 100 deletions(-) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index 368e2a4e65..bb48a76185 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -102,6 +102,10 @@ padding-right: 5px; border-right: 1px solid @at-gray-b7; user-select: none; + + &-clickable { + cursor: pointer; + } } &-event { diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index b9b925f2cc..68831f2a1e 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -1,11 +1,13 @@ import Ansi from 'ansi-to-html'; import Entities from 'html-entities'; +import getUUID from 'uuid'; import { EVENT_START_PLAY, EVENT_STATS_PLAY, EVENT_START_TASK, OUTPUT_ELEMENT_TBODY, + OUTPUT_EVENT_LIMIT, } from './constants'; const EVENT_GROUPS = [ @@ -31,46 +33,71 @@ const pattern = [ const re = new RegExp(pattern); const hasAnsi = input => re.test(input); -const MISSING_EVENT_GROUP = 'MISSING_EVENT_GROUP'; - function JobRenderService ($q, $sce, $window) { this.init = ({ compile, toggles }) => { - this.parent = null; - this.record = {}; - this.el = $(OUTPUT_ELEMENT_TBODY); this.hooks = { compile }; - this.createToggles = toggles; - this.lastMissing = false; + this.el = $(OUTPUT_ELEMENT_TBODY); + this.parent = null; + this.state = { - collapseAll: false + head: null, + tail: null, + collapseAll: false, + toggleMode: toggles, }; + + this.counters = {}; + this.lines = {}; + this.records = {}; + this.uuids = {}; + + this.missingCounterRecords = {}; + this.missingCounterUUIDs = {}; }; this.setCollapseAll = value => { this.state.collapseAll = value; }; - this.sortByLineNumber = (a, b) => { - if (a.start_line > b.start_line) { + this.sortByCounter = (a, b) => { + if (a.counter > b.counter) { return 1; } - if (a.start_line < b.start_line) { + if (a.counter < b.counter) { return -1; } return 0; }; - this.transformEventGroup = events => { + // + // Event Data Transformation / HTML Building + // + + this.transformEventGroup = (events, streaming = false) => { let lines = 0; let html = ''; - events.sort(this.sortByLineNumber); + events.sort(this.sortByCounter); + + for (let i = 0; i <= events.length - 1; i++) { + const current = events[i]; + + if (streaming) { + const tailCounter = this.getTailCounter(); + + if (tailCounter && (current.counter !== tailCounter + 1)) { + const missing = this.transformMissingEventGroup(current); + + html += missing.html; + lines += missing.count; + } + } + + const line = this.transformEvent(current); - for (let i = 0; i < events.length; ++i) { - const line = this.transformEvent(events[i]); html += line.html; lines += line.count; } @@ -78,21 +105,42 @@ function JobRenderService ($q, $sce, $window) { return { html, lines }; }; - this.transformEvent = event => { - if (event.uuid && this.record[event.uuid]) { + this.transformMissingEventGroup = event => { + const tail = this.lookupRecord(this.getTailCounter()); + + if (!tail || !tail.counter) { return { html: '', count: 0 }; } - if (event.event === MISSING_EVENT_GROUP) { - if (this.lastMissing) { - return { html: '', count: 0 }; - } + const uuid = getUUID(); + const counters = []; - this.lastMissing = true; - return this.transformMissingEvent(event); + for (let i = tail.counter + 1; i < event.counter; i++) { + counters.push(i); + this.missingCounterUUIDs[i] = uuid; } - this.lastMissing = false; + const record = { + counters, + uuid, + start: tail.end, + end: event.start_line, + }; + + this.missingCounterRecords[uuid] = record; + + const html = `
+
+
...
`; + const count = 1; + + return { html, count }; + }; + + this.transformEvent = event => { + if (event.uuid && this.records[event.uuid]) { + return { html: '', count: 0 }; + } if (!event || !event.stdout) { return { html: '', count: 0 }; @@ -100,59 +148,65 @@ function JobRenderService ($q, $sce, $window) { const stdout = this.sanitize(event.stdout); const lines = stdout.split('\r\n'); + const record = this.createRecord(event, lines); + let html = ''; let count = lines.length; let ln = event.start_line; - const current = this.createRecord(ln, lines, event); - - const html = lines.reduce((concat, line, i) => { + for (let i = 0; i <= lines.length - 1; i++) { ln++; + const line = lines[i]; const isLastLine = i === lines.length - 1; - let row = this.createRow(current, ln, line); + let row = this.createRow(record, ln, line); - if (current && current.isTruncated && isLastLine) { - row += this.createRow(current); + if (record && record.isTruncated && isLastLine) { + row += this.createRow(record); count++; } - return `${concat}${row}`; - }, ''); + html += row; + } return { html, count }; }; - this.transformMissingEvent = () => { - const html = '
...
'; - const count = 1; - - return { html, count }; - }; - - this.isHostEvent = (event) => { - if (typeof event.host === 'number') { - return true; + this.createRecord = (event, lines) => { + if (!event.counter) { + return null; } - if (event.type === 'project_update_event' && - event.event !== 'runner_on_skipped' && - event.event_data.host) { - return true; + this.lines[event.counter] = event.end_line - event.start_line; + + if (this.state.tail === null || + this.state.tail < event.counter) { + this.state.tail = event.counter; } - return false; - }; + if (this.state.head === null || + this.state.head > event.counter) { + this.state.head = event.counter; + } - this.createRecord = (ln, lines, event) => { if (!event.uuid) { return null; } - const info = { + 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, - line: ln + 1, + line: event.start_line + 1, name: event.event, uuid: event.uuid, level: event.event_level, @@ -160,54 +214,50 @@ function JobRenderService ($q, $sce, $window) { end: event.end_line, isTruncated: (event.end_line - event.start_line) > lines.length, lineCount: lines.length, - isHost: this.isHostEvent(event), isCollapsed: this.state.collapseAll, + counter: event.counter, }; if (event.parent_uuid) { - info.parents = this.getParentEvents(event.parent_uuid); - if (this.record[event.parent_uuid]) { - info.isCollapsed = this.record[event.parent_uuid].isCollapsed; + record.parents = this.getParentEvents(event.parent_uuid); + if (this.records[event.parent_uuid]) { + record.isCollapsed = this.records[event.parent_uuid].isCollapsed; } } - if (info.isTruncated) { - info.truncatedAt = event.start_line + lines.length; + if (record.isTruncated) { + record.truncatedAt = event.start_line + lines.length; } if (EVENT_GROUPS.includes(event.event)) { - info.isParent = true; + record.isParent = true; if (event.event_level === 1) { this.parent = event.uuid; } if (event.parent_uuid) { - if (this.record[event.parent_uuid]) { - if (this.record[event.parent_uuid].children && - !this.record[event.parent_uuid].children.includes(event.uuid)) { - this.record[event.parent_uuid].children.push(event.uuid); + if (this.records[event.parent_uuid]) { + if (this.records[event.parent_uuid].children && + !this.records[event.parent_uuid].children.includes(event.uuid)) { + this.records[event.parent_uuid].children.push(event.uuid); } else { - this.record[event.parent_uuid].children = [event.uuid]; + this.records[event.parent_uuid].children = [event.uuid]; } } } } if (TIME_EVENTS.includes(event.event)) { - info.time = this.getTimestamp(event.created); - info.line++; + record.time = this.getTimestamp(event.created); + record.line++; } - this.record[event.uuid] = info; + this.uuids[event.counter] = record.uuid; + this.counters[event.uuid] = record.counter; + this.records[event.uuid] = record; - return info; - }; - - this.getRecord = uuid => this.record[uuid]; - - this.deleteRecord = uuid => { - delete this.record[uuid]; + return record; }; this.getParentEvents = (uuid, list) => { @@ -215,14 +265,32 @@ function JobRenderService ($q, $sce, $window) { // always push its parent if exists list.push(uuid); // if we can get grandparent in current visible lines, we also push it - if (this.record[uuid] && this.record[uuid].parents) { - list = list.concat(this.record[uuid].parents); + if (this.records[uuid] && this.records[uuid].parents) { + list = list.concat(this.records[uuid].parents); } return list; }; - this.createRow = (current, ln, content) => { + this.deleteRecord = counter => { + const uuid = this.uuids[counter]; + + delete this.records[uuid]; + delete this.counters[uuid]; + delete this.uuids[counter]; + delete this.lines[counter]; + }; + + this.getTimestamp = created => { + const date = new Date(created); + const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); + const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); + const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); + + return `${hour}:${minute}:${second}`; + }; + + this.createRow = (record, ln, content) => { let id = ''; let icon = ''; let timestamp = ''; @@ -236,11 +304,11 @@ function JobRenderService ($q, $sce, $window) { content = ansi.toHtml(content); } - if (current) { - if (this.createToggles && current.isParent && current.line === ln) { - id = current.uuid; + if (record) { + if (this.state.toggleMode && record.isParent && record.line === ln) { + id = record.uuid; - if (current.isCollapsed) { + if (record.isCollapsed) { icon = 'fa-angle-right'; } else { icon = 'fa-angle-down'; @@ -249,16 +317,16 @@ function JobRenderService ($q, $sce, $window) { tdToggle = `
`; } - if (current.isHost) { - tdEvent = `
${content}
`; + if (record.isHost) { + tdEvent = `
${content}
`; } - if (current.time && current.line === ln) { - timestamp = `${current.time}`; + if (record.time && record.line === ln) { + timestamp = `${record.time}`; } - if (current.parents) { - classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); + if (record.parents) { + classList = record.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, ''); } } @@ -274,8 +342,8 @@ function JobRenderService ($q, $sce, $window) { ln = '...'; } - if (current && current.isCollapsed) { - if (current.level === 3 || current.level === 0) { + if (record && record.isCollapsed) { + if (record.level === 3 || record.level === 0) { classList += ' hidden'; } } @@ -289,14 +357,9 @@ function JobRenderService ($q, $sce, $window) { `; }; - this.getTimestamp = created => { - const date = new Date(created); - const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); - const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); - const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); - - return `${hour}:${minute}:${second}`; - }; + // + // Element Operations + // this.remove = elements => this.requestAnimationFrame(() => elements.remove()); @@ -316,7 +379,7 @@ function JobRenderService ($q, $sce, $window) { return this.requestAnimationFrame(); }; - this.clear = () => { + this.removeAll = () => { const elements = this.el.children(); return this.remove(elements); }; @@ -348,12 +411,12 @@ function JobRenderService ($q, $sce, $window) { .then(() => result.lines); }; - this.append = events => { + this.append = (events, streaming = false) => { if (events.length < 1) { return $q.resolve(); } - const result = this.transformEventGroup(events); + const result = this.transformEventGroup(events, streaming); const html = this.trustHtml(result.html); const newElements = angular.element(html); @@ -364,8 +427,189 @@ function JobRenderService ($q, $sce, $window) { }; this.trustHtml = html => $sce.getTrustedHtml($sce.trustAsHtml(html)); - this.sanitize = html => entities.encode(html); + + // + // Event Counter Methods - External code should use these. + // + + 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.getCapacity = () => OUTPUT_EVENT_LIMIT - Object.keys(this.lines).length; + + this.lookupRecord = counter => this.records[this.uuids[counter]]; + + this.getLineCount = counter => { + const record = this.lookupRecord(counter); + + if (record && record.lineCount) { + return record.lineCount; + } + + if (this.lines[counter]) { + return this.lines[counter]; + } + + return 0; + }; + + this.deleteMissingCounterRecord = counter => { + const uuid = this.missingCounterUUIDs[counter]; + + delete this.missingCounterRecords[counter]; + delete this.missingCounterUUIDs[uuid]; + }; + + this.clear = () => this.removeAll() + .then(() => { + const head = this.getHeadCounter(); + const tail = this.getTailCounter(); + + for (let i = head; i <= tail; ++i) { + this.deleteRecord(i); + this.deleteMissingCounterRecord(i); + } + + this.state.head = null; + this.state.tail = null; + + return $q.resolve(); + }); + + this.pushFront = (events, streaming = false) => { + const tail = this.getTailCounter(); + + return this.append(events.filter(({ counter }) => counter > tail), streaming); + }; + + this.pushBack = events => { + const head = this.getHeadCounter(); + const tail = this.getTailCounter(); + + return this.prepend(events.filter(({ counter }) => counter < head || counter > tail)); + }; + + this.pushFrames = events => this.pushFront(events, true); + + this.popMissing = counter => { + const uuid = this.missingCounterUUIDs[counter]; + + if (!this.missingCounterRecords[uuid]) { + return 0; + } + + this.missingCounterRecords[uuid].counters.pop(); + + if (this.missingCounterRecords[uuid].counters.length > 0) { + return 0; + } + + delete this.missingCounterRecords[uuid]; + delete this.missingCounterUUIDs[counter]; + + return 1; + }; + + this.shiftMissing = counter => { + const uuid = this.missingCounterUUIDs[counter]; + + if (!this.missingCounterRecords[uuid]) { + return 0; + } + + this.missingCounterRecords[uuid].counters.shift(); + + if (this.missingCounterRecords[uuid].counters.length > 0) { + return 0; + } + + delete this.missingCounterRecords[uuid]; + delete this.missingCounterUUIDs[counter]; + + return 1; + }; + + this.isCounterMissing = counter => this.missingCounterUUIDs[counter]; + + 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) { + if (this.isCounterMissing(i)) { + lines += this.popMissing(i); + } else { + lines += this.getLineCount(i); + } + } + + return this.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) { + if (this.isCounterMissing(i)) { + lines += this.popMissing(i); + } else { + lines += this.getLineCount(i); + } + } + + return this.shift(lines) + .then(() => { + for (let i = min; i <= max; ++i) { + this.deleteRecord(i); + this.state.head++; + } + + return $q.resolve(); + }); + }; } JobRenderService.$inject = ['$q', '$sce', '$window']; From 138f8a45ae2eb315e45985f2fda4d7813150ddeb Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 19 Aug 2018 21:57:13 -0400 Subject: [PATCH 04/10] moving render/record keeping and scroll functionality out of pagers --- .../features/output/index.controller.js | 297 ++++++++++++++--- awx/ui/client/features/output/page.service.js | 309 ++++++------------ .../client/features/output/slide.service.js | 301 ++--------------- 3 files changed, 394 insertions(+), 513 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 0f0579e14b..6e11152b76 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -17,12 +17,12 @@ let scroll; let status; let slide; let stream; +let page; let vm; - const listeners = []; +let lockFrames = false; -let lockFrames; function onFrames (events) { events = slide.pushFrames(events); @@ -30,7 +30,7 @@ function onFrames (events) { return $q.resolve(); } - const popCount = events.length - slide.getCapacity(); + const popCount = events.length - render.getCapacity(); const isAttached = events.length > 0; if (!isAttached) { @@ -52,13 +52,13 @@ function onFrames (events) { scroll.scrollToBottom(); } - return slide.popBack(popCount) + return render.popBack(popCount) .then(() => { if (vm.isFollowing) { scroll.scrollToBottom(); } - return slide.pushFront(events); + return render.pushFrames(events); }) .then(() => { if (vm.isFollowing) { @@ -71,7 +71,11 @@ function onFrames (events) { }); } -function first () { +// +// Menu Controls (Running) +// + +function firstRange () { if (scroll.isPaused()) { return $q.resolve(); } @@ -81,9 +85,15 @@ function first () { stopFollowing(); - return slide.getFirst() - .then(() => { - scroll.resetScrollPosition(); + return render.clear() + .then(() => slide.getFirst()) + .then(results => render.pushFront(results)) + .then(() => slide.getNext()) + .then(results => { + const popCount = results.length - render.getCapacity(); + + return render.popBack(popCount) + .then(() => render.pushFront(results)); }) .finally(() => { scroll.resume(); @@ -91,7 +101,7 @@ function first () { }); } -function next () { +function nextRange () { if (vm.isFollowing) { scroll.scrollToBottom(); @@ -110,26 +120,43 @@ function next () { lockFrames = true; return slide.getNext() + .then(results => { + const popCount = results.length - render.getCapacity(); + + return render.popBack(popCount) + .then(() => render.pushFront(results)); + }) .finally(() => { scroll.resume(); lockFrames = false; }); } -function previous () { +function previousRange () { if (scroll.isPaused()) { return $q.resolve(); } scroll.pause(); lockFrames = true; - stopFollowing(); - const initialPosition = scroll.getScrollPosition(); + let initialPosition; + let popHeight; 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(); scroll.setScrollPosition(currentHeight - popHeight + initialPosition); @@ -138,10 +165,12 @@ function previous () { .finally(() => { scroll.resume(); lockFrames = false; + + return $q.resolve(); }); } -function last () { +function lastRange () { if (scroll.isPaused()) { return $q.resolve(); } @@ -149,9 +178,10 @@ function last () { scroll.pause(); lockFrames = true; - return slide.getLast() + return render.clear() + .then(() => slide.getLast()) + .then(results => render.pushFront(results)) .then(() => { - stream.setMissingCounterThreshold(slide.getTailCounter() + 1); scroll.scrollToBottom(); return $q.resolve(); @@ -159,9 +189,30 @@ function last () { .finally(() => { scroll.resume(); lockFrames = false; + + return $q.resolve(); }); } +function menuLastRange () { + if (vm.isFollowing) { + lockFollow = true; + stopFollowing(); + + return $q.resolve(); + } + + lockFollow = false; + + if (slide.isOnLastPage()) { + scroll.scrollToBottom(); + + return $q.resolve(); + } + + return last(); +} + let followOnce; let lockFollow; function canStartFollowing () { @@ -207,23 +258,159 @@ function stopFollowing () { 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 () { - if (vm.isFollowing) { - lockFollow = true; - stopFollowing(); - - return $q.resolve(); + if (vm.isProcessingFinished) { + return lastPage(); } - lockFollow = false; - - if (slide.isOnLastPage()) { - scroll.scrollToBottom(); - - return $q.resolve(); - } - - return last(); + return menuLastRange(); } function down () { @@ -238,6 +425,10 @@ function togglePanelExpand () { vm.isPanelExpanded = !vm.isPanelExpanded; } +// +// Line Interaction +// + const iconCollapsed = 'fa-angle-right'; const iconExpanded = 'fa-angle-down'; const iconSelector = '.at-Stdout-toggle > i'; @@ -246,7 +437,7 @@ const lineCollapsed = 'hidden'; function toggleCollapseAll () { 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 tasks = records.filter(({ name }) => name === EVENT_START_TASK); @@ -286,7 +477,7 @@ function toggleCollapseAll () { function toggleCollapse (uuid) { if (scroll.isPaused()) return; - const record = render.record[uuid]; + const record = render.records[uuid]; if (record.name === EVENT_START_PLAY) { togglePlayCollapse(uuid); @@ -298,7 +489,7 @@ function toggleCollapse (uuid) { } function togglePlayCollapse (uuid) { - const record = render.record[uuid]; + const record = render.records[uuid]; const descendants = record.children || []; const icon = $(`#${uuid} ${iconSelector}`); @@ -329,11 +520,11 @@ function togglePlayCollapse (uuid) { } descendants - .map(item => render.record[item]) + .map(item => render.records[item]) .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) { @@ -352,7 +543,7 @@ function toggleTaskCollapse (uuid) { lines.addClass(lineCollapsed); } - render.record[uuid].isCollapsed = !isCollapsed; + render.records[uuid].isCollapsed = !isCollapsed; } function compile (html) { @@ -363,6 +554,14 @@ function showHostDetails (id, uuid) { $state.go('output.host-event.json', { eventId: id, taskUuid: uuid }); } +function showMissingEvents (uuid) { + console.log(`expandMissingEvents: ${uuid}`); +} + +// +// Event Handling +// + let streaming; function stopListening () { streaming = null; @@ -433,12 +632,20 @@ function handleSummaryEvent (data) { stream.setFinalCounter(data.final_counter); } +// +// Search +// + function reloadState (params) { params.isPanelExpanded = vm.isPanelExpanded; return $state.transitionTo($state.current, params, { inherit: false, location: 'replace' }); } +// +// Debug Mode +// + function clear () { stopListening(); render.clear(); @@ -449,7 +656,7 @@ function clear () { stream.bufferInit(); status.init(resource); - slide.init(render, resource.events, scroll); + slide.init(resource.events, render); status.subscribe(data => { vm.status = data.status; }); startListening(); @@ -484,7 +691,8 @@ function OutputIndexController ( render = _render_; status = _status_; stream = _stream_; - slide = isProcessingFinished ? _page_ : _slide_; + slide = _slide_; + page = _page_; vm = this || {}; @@ -495,6 +703,7 @@ function OutputIndexController ( vm.resource = resource; vm.reloadState = reloadState; vm.isPanelExpanded = isPanelExpanded; + vm.isProcessingFinished = isProcessingFinished; vm.togglePanelExpand = togglePanelExpand; // Stdout Navigation @@ -504,15 +713,18 @@ function OutputIndexController ( vm.toggleCollapseAll = toggleCollapseAll; vm.toggleCollapse = toggleCollapse; vm.showHostDetails = showHostDetails; + vm.showMissingEvents = showMissingEvents; vm.toggleLineEnabled = resource.model.get('type') === 'job'; vm.followTooltip = vm.strings.get('tooltips.MENU_LAST'); vm.debug = _debug; render.requestAnimationFrame(() => { - status.init(resource); - slide.init(render, resource.events, scroll); render.init({ compile, toggles: vm.toggleLineEnabled }); + status.init(resource); + page.init(resource.events); + slide.init(resource.events, render); + scroll.init({ next, previous, @@ -600,4 +812,3 @@ OutputIndexController.$inject = [ ]; module.exports = OutputIndexController; - diff --git a/awx/ui/client/features/output/page.service.js b/awx/ui/client/features/output/page.service.js index 786d26ad66..1502d39a63 100644 --- a/awx/ui/client/features/output/page.service.js +++ b/awx/ui/client/features/output/page.service.js @@ -2,244 +2,153 @@ import { OUTPUT_PAGE_LIMIT } from './constants'; function PageService ($q) { - this.init = (storage, api, { getScrollHeight }) => { - const { prepend, append, shift, pop, deleteRecord } = storage; - const { getPage, getFirst, getLast, getLastPageNumber, getMaxCounter } = api; - + this.init = ({ getPage, getFirst, getLast, getLastPageNumber }) => { this.api = { getPage, getFirst, getLast, getLastPageNumber, - getMaxCounter, }; - - this.storage = { - 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.pages = {}; + this.state = { head: 0, tail: 0 }; }; this.getNext = () => { const lastPageNumber = this.api.getLastPageNumber(); const number = Math.min(this.state.tail + 1, lastPageNumber); - const isLoaded = (number >= this.state.head && number <= this.state.tail); - const isValid = (number >= 1 && number <= lastPageNumber); - - let popHeight = this.hooks.getScrollHeight(); - - if (!isValid || isLoaded) { - this.chain = this.chain - .then(() => $q.resolve(popHeight)); - - return this.chain; + if (number < 1) { + return $q.resolve([]); } - const pageCount = this.state.head - this.state.tail; - - if (pageCount >= OUTPUT_PAGE_LIMIT) { - this.chain = this.chain - .then(() => this.popBack()) - .then(() => { - popHeight = this.hooks.getScrollHeight(); - - return $q.resolve(); - }); + if (number > lastPageNumber) { + return $q.resolve([]); } - this.chain = this.chain - .then(() => this.api.getPage(number)) - .then(events => this.pushFront(events)) - .then(() => $q.resolve(popHeight)); + let promise; - 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 = () => { const number = Math.max(this.state.head - 1, 1); - const isLoaded = (number >= this.state.head && number <= this.state.tail); - const isValid = (number >= 1 && number <= this.api.getLastPageNumber()); - - let popHeight = this.hooks.getScrollHeight(); - - if (!isValid || isLoaded) { - this.chain = this.chain - .then(() => $q.resolve(popHeight)); - - return this.chain; + if (number < 1) { + return $q.resolve([]); } - const pageCount = this.state.head - this.state.tail; - - if (pageCount >= OUTPUT_PAGE_LIMIT) { - this.chain = this.chain - .then(() => this.popFront()) - .then(() => { - popHeight = this.hooks.getScrollHeight(); - - return $q.resolve(); - }); + if (number > this.api.getLastPageNumber()) { + return $q.resolve([]); } - this.chain = this.chain - .then(() => this.api.getPage(number)) - .then(events => this.pushBack(events)) - .then(() => $q.resolve(popHeight)); + let promise; - 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 = () => { - const count = this.getRecordCount(); + this.getLast = () => this.api.getLast() + .then(results => { + if (results.length <= 0) { + return $q.resolve([]); + } - for (let i = 0; i <= count; ++i) { - this.chain = this.chain.then(() => this.popBack()); - } + const number = this.api.getLastPageNumber(); - return this.chain; - }; + this.state.head = number; + this.state.tail = number; + this.pages[number] = results; - this.getLast = () => this.clear() - .then(() => this.api.getLast()) - .then(events => { - const lastPage = this.api.getLastPageNumber(); + return $q.resolve(results); + }); - this.state.head = lastPage; - this.state.tail = lastPage; + this.getFirst = () => this.api.getFirst() + .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.tail = 1; + this.pages[1] = results; - return this.pushBack(events, 1); - }) - .then(() => this.getNext()); + return $q.resolve(results); + }); - this.isOnLastPage = () => this.api.getLastPageNumber() === this.state.tail; - this.getRecordCount = () => Object.keys(this.records).length; - this.getTailCounter = () => this.state.tail; - this.getMaxCounter = () => this.api.getMaxCounter(); + this.trimTail = () => { + const { tail, head } = this.state; + let popCount = 0; + + 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']; diff --git a/awx/ui/client/features/output/slide.service.js b/awx/ui/client/features/output/slide.service.js index df6105617e..63a1753972 100644 --- a/awx/ui/client/features/output/slide.service.js +++ b/awx/ui/client/features/output/slide.service.js @@ -1,6 +1,5 @@ /* eslint camelcase: 0 */ import { - API_MAX_PAGE_SIZE, OUTPUT_EVENT_LIMIT, OUTPUT_PAGE_SIZE, } from './constants'; @@ -34,9 +33,8 @@ function getContinuous (events, reverse = false) { } function SlidingWindowService ($q) { - this.init = (storage, api, { getScrollHeight }) => { - const { prepend, append, shift, pop, getRecord, deleteRecord, clear } = storage; - const { getRange, getFirst, getLast, getMaxCounter } = api; + this.init = ({ getRange, getFirst, getLast, getMaxCounter }, storage) => { + const { getHeadCounter, getTailCounter } = storage; this.api = { getRange, @@ -46,36 +44,24 @@ function SlidingWindowService ($q) { }; this.storage = { - clear, - prepend, - append, - shift, - pop, - getRecord, - deleteRecord, + getHeadCounter, + getTailCounter, }; - this.hooks = { - getScrollHeight, - }; - - this.lines = {}; - this.uuids = {}; - this.chain = $q.resolve(); - - this.state = { head: null, tail: null }; - this.cache = { first: null }; - this.buffer = { events: [], min: 0, max: 0, count: 0, }; + + this.cache = { + first: null + }; }; this.getBoundedRange = range => { - const bounds = [0, this.getMaxCounter()]; + const bounds = [1, this.getMaxCounter()]; return [Math.max(range[0], bounds[0]), Math.min(range[1], bounds[1])]; }; @@ -92,273 +78,48 @@ function SlidingWindowService ($q) { 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) => { const next = this.getNextRange(displacement); - const [head, tail] = this.getRange(); - this.chain = this.chain - .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; + return this.api.getRange(next) + .then(results => getContinuous(results)); }; this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => { const previous = this.getPreviousRange(displacement); - const [head, tail] = this.getRange(); - this.chain = this.chain - .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; + return this.api.getRange(previous) + .then(results => getContinuous(results, true)); }; this.getFirst = () => { - this.chain = this.chain - .then(() => this.clear()) - .then(() => { - if (this.cache.first) { - return $q.resolve(this.cache.first); - } + if (this.cache.first) { + return $q.resolve(this.cache.first); + } - return this.api.getFirst(); - }) + return this.api.getFirst() .then(events => { if (events.length === OUTPUT_PAGE_SIZE) { this.cache.first = events; } - return this.pushFront(events); + return $q.resolve(events); }); - - return this.chain - .then(() => this.getNext()); }; - this.getLast = () => { - this.chain = this.chain - .then(() => this.getFrames()) - .then(frames => { - if (frames.length > 0) { - return $q.resolve(frames); - } + this.getLast = () => this.getFrames() + .then(frames => { + if (frames.length > 0) { + return $q.resolve(frames); + } - 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; - }; + return this.api.getLast(); + }); this.pushFrames = events => { + const head = this.getHeadCounter(); + const tail = this.getTailCounter(); const frames = this.buffer.events.concat(events); - const [head, tail] = this.getRange(); let min; let max; @@ -367,7 +128,7 @@ function SlidingWindowService ($q) { for (let i = frames.length - 1; i >= 0; i--) { count++; - if (count > API_MAX_PAGE_SIZE) { + if (count > OUTPUT_EVENT_LIMIT) { frames.splice(i, 1); count--; @@ -417,9 +178,9 @@ function SlidingWindowService ($q) { return this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); }; - this.getRange = () => [this.getHeadCounter(), this.getTailCounter()]; - this.getRecordCount = () => Object.keys(this.lines).length; - this.getCapacity = () => OUTPUT_EVENT_LIMIT - this.getRecordCount(); + this.isOnFirstPage = () => this.getHeadCounter() === 1; + this.getTailCounter = () => this.storage.getTailCounter(); + this.getHeadCounter = () => this.storage.getHeadCounter(); } SlidingWindowService.$inject = ['$q']; From 0bc9b1d431d495837618cc8380cfbc07b673d2cd Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 20 Aug 2018 23:20:40 -0400 Subject: [PATCH 05/10] render missing lines instead of auto-unfollowing --- .../features/output/index.controller.js | 44 +++--- .../client/features/output/render.service.js | 144 +++++++++++++++--- .../client/features/output/slide.service.js | 54 ++----- 3 files changed, 154 insertions(+), 88 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index 6e11152b76..f314a28afc 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -31,12 +31,6 @@ function onFrames (events) { } const popCount = events.length - render.getCapacity(); - const isAttached = events.length > 0; - - if (!isAttached) { - stopFollowing(); - return $q.resolve(); - } if (!vm.isFollowing && canStartFollowing()) { startFollowing(); @@ -58,7 +52,7 @@ function onFrames (events) { scroll.scrollToBottom(); } - return render.pushFrames(events); + return render.pushFront(events); }) .then(() => { if (vm.isFollowing) { @@ -80,11 +74,18 @@ function firstRange () { return $q.resolve(); } + stopFollowing(); + lockFollow = true; + + if (slide.isOnFirstPage()) { + scroll.resetScrollPosition(); + + return $q.resolve(); + } + scroll.pause(); lockFrames = true; - stopFollowing(); - return render.clear() .then(() => slide.getFirst()) .then(results => render.pushFront(results)) @@ -97,7 +98,7 @@ function firstRange () { }) .finally(() => { scroll.resume(); - lockFrames = false; + lockFollow = false; }); } @@ -112,10 +113,6 @@ function nextRange () { return $q.resolve(); } - if (slide.getTailCounter() >= slide.getMaxCounter()) { - return $q.resolve(); - } - scroll.pause(); lockFrames = true; @@ -129,6 +126,8 @@ function nextRange () { .finally(() => { scroll.resume(); lockFrames = false; + + return $q.resolve(); }); } @@ -138,8 +137,8 @@ function previousRange () { } scroll.pause(); - lockFrames = true; stopFollowing(); + lockFrames = true; let initialPosition; let popHeight; @@ -182,13 +181,15 @@ function lastRange () { .then(() => slide.getLast()) .then(results => render.pushFront(results)) .then(() => { + stream.setMissingCounterThreshold(slide.getTailCounter() + 1); + scroll.scrollToBottom(); + lockFrames = false; return $q.resolve(); }) .finally(() => { scroll.resume(); - lockFrames = false; return $q.resolve(); }); @@ -204,13 +205,12 @@ function menuLastRange () { lockFollow = false; - if (slide.isOnLastPage()) { - scroll.scrollToBottom(); + return lastRange() + .then(() => { + startFollowing(); - return $q.resolve(); - } - - return last(); + return $q.resolve(); + }); } let followOnce; diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 68831f2a1e..7d98a1be2c 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -58,6 +58,9 @@ function JobRenderService ($q, $sce, $window) { this.setCollapseAll = value => { this.state.collapseAll = value; + Object.keys(this.records).forEach(key => { + this.records[key].isCollapsed = value; + }); }; this.sortByCounter = (a, b) => { @@ -76,7 +79,7 @@ function JobRenderService ($q, $sce, $window) { // Event Data Transformation / HTML Building // - this.transformEventGroup = (events, streaming = false) => { + this.appendEventGroup = events => { let lines = 0; let html = ''; @@ -84,16 +87,13 @@ function JobRenderService ($q, $sce, $window) { for (let i = 0; i <= events.length - 1; i++) { const current = events[i]; + const tailCounter = this.getTailCounter(); - if (streaming) { - const tailCounter = this.getTailCounter(); + if (tailCounter && (current.counter !== tailCounter + 1)) { + const missing = this.appendMissingEventGroup(current); - if (tailCounter && (current.counter !== tailCounter + 1)) { - const missing = this.transformMissingEventGroup(current); - - html += missing.html; - lines += missing.count; - } + html += missing.html; + lines += missing.count; } const line = this.transformEvent(current); @@ -105,21 +105,39 @@ function JobRenderService ($q, $sce, $window) { return { html, lines }; }; - this.transformMissingEventGroup = event => { - const tail = this.lookupRecord(this.getTailCounter()); + this.appendMissingEventGroup = event => { + const tailCounter = this.getTailCounter(); + const tail = this.lookupRecord(tailCounter); + const tailMissing = this.isCounterMissing(tailCounter); - if (!tail || !tail.counter) { + if (!tailMissing && (!tail || !tail.counter)) { return { html: '', count: 0 }; } - const uuid = getUUID(); + let uuid; + + if (tailMissing) { + uuid = this.missingCounterUUIDs[tailCounter]; + } else { + uuid = getUUID(); + } + const counters = []; - for (let i = tail.counter + 1; i < event.counter; i++) { - counters.push(i); + for (let i = tailCounter + 1; i < event.counter; i++) { + if (tailMissing) { + this.missingCounterRecords[uuid].counters.push(i); + } else { + counters.push(i); + } + this.missingCounterUUIDs[i] = uuid; } + if (tailMissing) { + return { html: '', count: 0 }; + } + const record = { counters, uuid, @@ -127,6 +145,90 @@ function JobRenderService ($q, $sce, $window) { end: event.start_line, }; + if (record.start === record.end) { + return { html: '', count: 0 }; + } + + this.missingCounterRecords[uuid] = record; + + const html = `
+
+
...
`; + 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]; + const headCounter = this.getHeadCounter(); + + if (headCounter && (current.counter !== headCounter - 1)) { + const missing = this.prependMissingEventGroup(current); + + html = missing.html + html; + lines += missing.count; + } + + const line = this.transformEvent(current); + + html = line.html + html; + lines += line.count; + } + + return { html, lines }; + }; + + this.prependMissingEventGroup = event => { + const headCounter = this.getHeadCounter(); + const head = this.lookupRecord(headCounter); + const headMissing = this.isCounterMissing(headCounter); + + if (!headMissing && (!head || !head.counter)) { + return { html: '', count: 0 }; + } + + let uuid; + + if (headMissing) { + uuid = this.missingCounterUUIDs[headCounter]; + } else { + uuid = getUUID(); + } + + const counters = []; + + for (let i = headCounter - 1; i > event.counter; i--) { + if (headMissing) { + this.missingCounterRecords[uuid].counters.unshift(i); + } else { + counters.unshift(i); + } + + this.missingCounterUUIDs[i] = uuid; + } + + if (headMissing) { + return { html: '', count: 0 }; + } + + const record = { + counters, + uuid, + start: event.end_line, + end: head.start, + }; + + if (record.start === record.end) { + return { html: '', count: 0 }; + } + this.missingCounterRecords[uuid] = record; const html = `
@@ -401,7 +503,7 @@ function JobRenderService ($q, $sce, $window) { return $q.resolve(); } - const result = this.transformEventGroup(events); + const result = this.prependEventGroup(events); const html = this.trustHtml(result.html); const newElements = angular.element(html); @@ -411,12 +513,12 @@ function JobRenderService ($q, $sce, $window) { .then(() => result.lines); }; - this.append = (events, streaming = false) => { + this.append = events => { if (events.length < 1) { return $q.resolve(); } - const result = this.transformEventGroup(events, streaming); + const result = this.appendEventGroup(events); const html = this.trustHtml(result.html); const newElements = angular.element(html); @@ -498,10 +600,10 @@ function JobRenderService ($q, $sce, $window) { return $q.resolve(); }); - this.pushFront = (events, streaming = false) => { + this.pushFront = events => { const tail = this.getTailCounter(); - return this.append(events.filter(({ counter }) => counter > tail), streaming); + return this.append(events.filter(({ counter }) => counter > tail)); }; this.pushBack = events => { @@ -511,8 +613,6 @@ function JobRenderService ($q, $sce, $window) { return this.prepend(events.filter(({ counter }) => counter < head || counter > tail)); }; - this.pushFrames = events => this.pushFront(events, true); - this.popMissing = counter => { const uuid = this.missingCounterUUIDs[counter]; diff --git a/awx/ui/client/features/output/slide.service.js b/awx/ui/client/features/output/slide.service.js index 63a1753972..fd052cc8fc 100644 --- a/awx/ui/client/features/output/slide.service.js +++ b/awx/ui/client/features/output/slide.service.js @@ -1,37 +1,9 @@ /* eslint camelcase: 0 */ import { - OUTPUT_EVENT_LIMIT, + OUTPUT_MAX_BUFFER_LENGTH, OUTPUT_PAGE_SIZE, } 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) { this.init = ({ getRange, getFirst, getLast, getMaxCounter }, storage) => { const { getHeadCounter, getTailCounter } = storage; @@ -81,15 +53,13 @@ function SlidingWindowService ($q) { this.getNext = (displacement = OUTPUT_PAGE_SIZE) => { const next = this.getNextRange(displacement); - return this.api.getRange(next) - .then(results => getContinuous(results)); + return this.api.getRange(next); }; this.getPrevious = (displacement = OUTPUT_PAGE_SIZE) => { const previous = this.getPreviousRange(displacement); - return this.api.getRange(previous) - .then(results => getContinuous(results, true)); + return this.api.getRange(previous); }; this.getFirst = () => { @@ -128,7 +98,7 @@ function SlidingWindowService ($q) { for (let i = frames.length - 1; i >= 0; i--) { count++; - if (count > OUTPUT_EVENT_LIMIT) { + if (count > OUTPUT_MAX_BUFFER_LENGTH) { frames.splice(i, 1); count--; @@ -153,29 +123,25 @@ function SlidingWindowService ($q) { return frames; } - if (min >= head && min <= tail + 1) { - return frames.filter(({ counter }) => counter > tail); - } - - return []; + return frames.filter(({ counter }) => counter > tail); }; this.getFrames = () => $q.resolve(this.buffer.events); this.getMaxCounter = () => { - if (this.buffer.min && this.buffer.min > 1) { - return this.buffer.min - 1; + if (this.buffer.max && this.buffer.max > 1) { + return this.buffer.max; } return this.api.getMaxCounter(); }; this.isOnLastPage = () => { - if (this.getTailCounter() === 0) { - return true; + if (this.buffer.min) { + return this.getTailCounter() >= this.buffer.min - 1; } - return this.getTailCounter() >= (this.getMaxCounter() - OUTPUT_PAGE_SIZE); + return this.getTailCounter() >= this.getMaxCounter() - OUTPUT_PAGE_SIZE; }; this.isOnFirstPage = () => this.getHeadCounter() === 1; From 04dbc2fcc41a36b3c7d59f9864420c0c8ca5b49b Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 23 Aug 2018 13:55:47 -0400 Subject: [PATCH 06/10] add basic click handler for fetching and showing missing events --- .../features/output/index.controller.js | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index f314a28afc..abb08b907e 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -555,7 +555,41 @@ function showHostDetails (id, uuid) { } function showMissingEvents (uuid) { - console.log(`expandMissingEvents: ${uuid}`); + const { counters } = render.records[uuid]; + + const min = Math.min(...counters); + const max = Math.min(Math.max(...counters), min + OUTPUT_PAGE_SIZE); + + const selector = `#${uuid}`; + const clicked = $(selector); + + return resource.events.getRange([min, max]) + .then(results => { + 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; + render.records[uuid].counters.shift(); + } + + 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(); + } + }) + .then(() => render.compile(elements)) + .then(() => lines); + }); } // From d608402dc13c598fb7bfbae82d8cd43166d755af Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sat, 25 Aug 2018 00:57:50 -0400 Subject: [PATCH 07/10] refactor render service --- .../client/features/output/render.service.js | 351 ++++++------------ 1 file changed, 112 insertions(+), 239 deletions(-) diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 7d98a1be2c..520324866f 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -1,6 +1,5 @@ import Ansi from 'ansi-to-html'; import Entities from 'html-entities'; -import getUUID from 'uuid'; import { EVENT_START_PLAY, @@ -36,24 +35,18 @@ const hasAnsi = input => re.test(input); function JobRenderService ($q, $sce, $window) { this.init = ({ compile, toggles }) => { this.hooks = { compile }; - this.el = $(OUTPUT_ELEMENT_TBODY); this.parent = null; this.state = { - head: null, - tail: null, + head: 0, + tail: 0, collapseAll: false, toggleMode: toggles, }; - this.counters = {}; - this.lines = {}; this.records = {}; this.uuids = {}; - - this.missingCounterRecords = {}; - this.missingCounterUUIDs = {}; }; this.setCollapseAll = value => { @@ -87,73 +80,54 @@ function JobRenderService ($q, $sce, $window) { for (let i = 0; i <= events.length - 1; i++) { const current = events[i]; - const tailCounter = this.getTailCounter(); - if (tailCounter && (current.counter !== tailCounter + 1)) { + if (this.state.tail && current.counter !== this.state.tail + 1) { const missing = this.appendMissingEventGroup(current); html += missing.html; lines += missing.count; } - const line = this.transformEvent(current); + const eventLines = this.transformEvent(current); - html += line.html; - lines += line.count; + html += eventLines.html; + lines += eventLines.count; } return { html, lines }; }; this.appendMissingEventGroup = event => { - const tailCounter = this.getTailCounter(); - const tail = this.lookupRecord(tailCounter); - const tailMissing = this.isCounterMissing(tailCounter); + const tailUUID = this.uuids[this.state.tail]; + const tailRecord = this.records[tailUUID]; - if (!tailMissing && (!tail || !tail.counter)) { + if (!tailRecord) { return { html: '', count: 0 }; } let uuid; - if (tailMissing) { - uuid = this.missingCounterUUIDs[tailCounter]; + if (tailRecord.isMissing) { + uuid = tailUUID; } else { - uuid = getUUID(); + uuid = `${event.counter}-${tailUUID}`; + this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true }; } - const counters = []; - - for (let i = tailCounter + 1; i < event.counter; i++) { - if (tailMissing) { - this.missingCounterRecords[uuid].counters.push(i); - } else { - counters.push(i); - } - - this.missingCounterUUIDs[i] = uuid; + for (let i = this.state.tail + 1; i < event.counter; i++) { + this.records[uuid].counters.push(i); + this.uuids[i] = uuid; } - if (tailMissing) { + if (tailRecord.isMissing) { return { html: '', count: 0 }; } - const record = { - counters, - uuid, - start: tail.end, - end: event.start_line, - }; - - if (record.start === record.end) { + if (tailRecord.end === event.start_line) { return { html: '', count: 0 }; } - this.missingCounterRecords[uuid] = record; - - const html = `
-
-
...
`; + const html = this.buildRowHTML(this.records[uuid]); const count = 1; return { html, count }; @@ -167,84 +141,65 @@ function JobRenderService ($q, $sce, $window) { for (let i = events.length - 1; i >= 0; i--) { const current = events[i]; - const headCounter = this.getHeadCounter(); - if (headCounter && (current.counter !== headCounter - 1)) { + if (this.state.head && current.counter !== this.state.head - 1) { const missing = this.prependMissingEventGroup(current); html = missing.html + html; lines += missing.count; } - const line = this.transformEvent(current); + const eventLines = this.transformEvent(current); - html = line.html + html; - lines += line.count; + html = eventLines.html + html; + lines += eventLines.count; } return { html, lines }; }; this.prependMissingEventGroup = event => { - const headCounter = this.getHeadCounter(); - const head = this.lookupRecord(headCounter); - const headMissing = this.isCounterMissing(headCounter); + const headUUID = this.uuids[this.state.head]; + const headRecord = this.records[headUUID]; - if (!headMissing && (!head || !head.counter)) { + if (!headRecord) { return { html: '', count: 0 }; } let uuid; - if (headMissing) { - uuid = this.missingCounterUUIDs[headCounter]; + if (headRecord.isMissing) { + uuid = headUUID; } else { - uuid = getUUID(); + uuid = `${headUUID}-${event.counter}`; + this.records[uuid] = { uuid, counters: [], lineCount: 1, isMissing: true }; } - const counters = []; - - for (let i = headCounter - 1; i > event.counter; i--) { - if (headMissing) { - this.missingCounterRecords[uuid].counters.unshift(i); - } else { - counters.unshift(i); - } - - this.missingCounterUUIDs[i] = uuid; + for (let i = this.state.head - 1; i > event.counter; i--) { + this.records[uuid].counters.unshift(i); + this.uuids[i] = uuid; } - if (headMissing) { + if (headRecord.isMissing) { return { html: '', count: 0 }; } - const record = { - counters, - uuid, - start: event.end_line, - end: head.start, - }; - - if (record.start === record.end) { + if (event.end_line === headRecord.start) { return { html: '', count: 0 }; } - this.missingCounterRecords[uuid] = record; - - const html = `
-
-
...
`; + const html = this.buildRowHTML(this.records[uuid]); const count = 1; return { html, count }; }; this.transformEvent = event => { - if (event.uuid && this.records[event.uuid]) { + if (!event || !event.stdout) { return { html: '', count: 0 }; } - if (!event || !event.stdout) { + if (event.uuid && this.records[event.uuid]) { return { html: '', count: 0 }; } @@ -262,10 +217,10 @@ function JobRenderService ($q, $sce, $window) { const line = lines[i]; const isLastLine = i === lines.length - 1; - let row = this.createRow(record, ln, line); + let row = this.buildRowHTML(record, ln, line); if (record && record.isTruncated && isLastLine) { - row += this.createRow(record); + row += this.buildRowHTML(record); count++; } @@ -280,20 +235,19 @@ function JobRenderService ($q, $sce, $window) { return null; } - this.lines[event.counter] = event.end_line - event.start_line; - - if (this.state.tail === null || - this.state.tail < event.counter) { - this.state.tail = event.counter; - } - - if (this.state.head === null || - this.state.head > event.counter) { + 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) { - return null; + this.uuids[event.counter] = event.counter; + this.records[event.counter] = { counters: [event.counter], lineCount: lines.length }; + + return this.records[event.counter]; } let isHost = false; @@ -317,7 +271,7 @@ function JobRenderService ($q, $sce, $window) { isTruncated: (event.end_line - event.start_line) > lines.length, lineCount: lines.length, isCollapsed: this.state.collapseAll, - counter: event.counter, + counters: [event.counter], }; if (event.parent_uuid) { @@ -355,9 +309,8 @@ function JobRenderService ($q, $sce, $window) { record.line++; } - this.uuids[event.counter] = record.uuid; - this.counters[event.uuid] = record.counter; this.records[event.uuid] = record; + this.uuids[event.counter] = event.uuid; return record; }; @@ -374,25 +327,7 @@ function JobRenderService ($q, $sce, $window) { return list; }; - this.deleteRecord = counter => { - const uuid = this.uuids[counter]; - - delete this.records[uuid]; - delete this.counters[uuid]; - delete this.uuids[counter]; - delete this.lines[counter]; - }; - - this.getTimestamp = created => { - const date = new Date(created); - const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); - const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); - const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); - - return `${hour}:${minute}:${second}`; - }; - - this.createRow = (record, ln, content) => { + this.buildRowHTML = (record, ln, content) => { let id = ''; let icon = ''; let timestamp = ''; @@ -400,6 +335,12 @@ function JobRenderService ($q, $sce, $window) { let tdEvent = ''; let classList = ''; + if (record.isMissing) { + return `
+
+
...
`; + } + content = content || ''; if (hasAnsi(content)) { @@ -459,6 +400,15 @@ function JobRenderService ($q, $sce, $window) {
`; }; + this.getTimestamp = created => { + const date = new Date(created); + const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours(); + const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes(); + const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds(); + + return `${hour}:${minute}:${second}`; + }; + // // Element Operations // @@ -532,70 +482,25 @@ function JobRenderService ($q, $sce, $window) { this.sanitize = html => entities.encode(html); // - // Event Counter Methods - External code should use these. + // Event Counter Methods - External code should prefer these. // - 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.getCapacity = () => OUTPUT_EVENT_LIMIT - Object.keys(this.lines).length; - - this.lookupRecord = counter => this.records[this.uuids[counter]]; - - this.getLineCount = counter => { - const record = this.lookupRecord(counter); - - if (record && record.lineCount) { - return record.lineCount; - } - - if (this.lines[counter]) { - return this.lines[counter]; - } - - return 0; - }; - - this.deleteMissingCounterRecord = counter => { - const uuid = this.missingCounterUUIDs[counter]; - - delete this.missingCounterRecords[counter]; - delete this.missingCounterUUIDs[uuid]; - }; - this.clear = () => this.removeAll() .then(() => { const head = this.getHeadCounter(); const tail = this.getTailCounter(); for (let i = head; i <= tail; ++i) { - this.deleteRecord(i); - this.deleteMissingCounterRecord(i); + const uuid = this.uuids[i]; + + if (uuid) { + delete this.records[uuid]; + delete this.uuids[i]; + } } - this.state.head = null; - this.state.tail = null; + this.state.head = 0; + this.state.tail = 0; return $q.resolve(); }); @@ -613,73 +518,35 @@ function JobRenderService ($q, $sce, $window) { return this.prepend(events.filter(({ counter }) => counter < head || counter > tail)); }; - this.popMissing = counter => { - const uuid = this.missingCounterUUIDs[counter]; - - if (!this.missingCounterRecords[uuid]) { - return 0; - } - - this.missingCounterRecords[uuid].counters.pop(); - - if (this.missingCounterRecords[uuid].counters.length > 0) { - return 0; - } - - delete this.missingCounterRecords[uuid]; - delete this.missingCounterUUIDs[counter]; - - return 1; - }; - - this.shiftMissing = counter => { - const uuid = this.missingCounterUUIDs[counter]; - - if (!this.missingCounterRecords[uuid]) { - return 0; - } - - this.missingCounterRecords[uuid].counters.shift(); - - if (this.missingCounterRecords[uuid].counters.length > 0) { - return 0; - } - - delete this.missingCounterRecords[uuid]; - delete this.missingCounterUUIDs[counter]; - - return 1; - }; - - this.isCounterMissing = counter => this.missingCounterUUIDs[counter]; - this.popFront = count => { if (!count || count <= 0) { return $q.resolve(); } - const max = this.getTailCounter(); + const max = this.state.tail; const min = max - count; let lines = 0; for (let i = max; i >= min; --i) { - if (this.isCounterMissing(i)) { - lines += this.popMissing(i); - } else { - lines += this.getLineCount(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) - .then(() => { - for (let i = max; i >= min; --i) { - this.deleteRecord(i); - this.state.tail--; - } - - return $q.resolve(); - }); + return this.pop(lines); }; this.popBack = count => { @@ -687,29 +554,35 @@ function JobRenderService ($q, $sce, $window) { return $q.resolve(); } - const min = this.getHeadCounter(); + const min = this.state.head; const max = min + count; let lines = 0; for (let i = min; i <= max; ++i) { - if (this.isCounterMissing(i)) { - lines += this.popMissing(i); - } else { - lines += this.getLineCount(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) - .then(() => { - for (let i = min; i <= max; ++i) { - this.deleteRecord(i); - this.state.head++; - } - - return $q.resolve(); - }); + 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']; From aa0d2cff5cfbfe2bdc0e97b8fd6f0b27d9959eba Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Sun, 26 Aug 2018 14:13:50 -0400 Subject: [PATCH 08/10] handle response data with discontinuities when using that data to fill other discontinuities --- .../features/output/index.controller.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index abb08b907e..4e59735f24 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -555,16 +555,25 @@ function showHostDetails (id, uuid) { } function showMissingEvents (uuid) { - const { counters } = render.records[uuid]; + const record = render.records[uuid]; - const min = Math.min(...counters); - const max = Math.min(Math.max(...counters), min + OUTPUT_PAGE_SIZE); + 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 = ''; @@ -573,7 +582,9 @@ function showMissingEvents (uuid) { lines += count; untrusted += html; - render.records[uuid].counters.shift(); + + const shifted = render.records[uuid].counters.shift(); + delete render.uuids[shifted]; } const trusted = render.trustHtml(untrusted); @@ -585,6 +596,7 @@ function showMissingEvents (uuid) { if (render.records[uuid].counters.length === 0) { clicked.remove(); + delete render.records[uuid]; } }) .then(() => render.compile(elements)) From 4e45b6ba6da0002834d589ad3d166533aca83067 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 27 Aug 2018 01:55:36 -0400 Subject: [PATCH 09/10] fix missing line styling --- awx/ui/client/features/output/_index.less | 6 +++--- awx/ui/client/features/output/render.service.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index bb48a76185..4684bb8b16 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -102,10 +102,10 @@ padding-right: 5px; border-right: 1px solid @at-gray-b7; user-select: none; + } - &-clickable { - cursor: pointer; - } + &-line--clickable { + cursor: pointer; } &-event { diff --git a/awx/ui/client/features/output/render.service.js b/awx/ui/client/features/output/render.service.js index 520324866f..7f297955fc 100644 --- a/awx/ui/client/features/output/render.service.js +++ b/awx/ui/client/features/output/render.service.js @@ -338,7 +338,7 @@ function JobRenderService ($q, $sce, $window) { if (record.isMissing) { return `
-
...
`; +
...
`; } content = content || ''; From f639e4671890452337616858d5dc43a4f20c61e2 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 29 Aug 2018 03:03:24 -0400 Subject: [PATCH 10/10] advance ready counter by an entire batch when event limit is reached --- awx/ui/client/features/output/constants.js | 2 +- awx/ui/client/features/output/stream.service.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/constants.js b/awx/ui/client/features/output/constants.js index 9f3b18c8cb..4e80e1d7bb 100644 --- a/awx/ui/client/features/output/constants.js +++ b/awx/ui/client/features/output/constants.js @@ -16,7 +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_TBODY = '#atStdoutResultTable'; export const OUTPUT_ELEMENT_LAST = '#atStdoutMenuLast'; -export const OUTPUT_MAX_BUFFER_LENGTH = 500; +export const OUTPUT_MAX_BUFFER_LENGTH = 1000; export const OUTPUT_MAX_LAG = 120; export const OUTPUT_NO_COUNT_JOB_TYPES = ['ad_hoc_command', 'system_job', 'inventory_update']; export const OUTPUT_ORDER_BY = 'counter'; diff --git a/awx/ui/client/features/output/stream.service.js b/awx/ui/client/features/output/stream.service.js index dc874ca0b9..9065d98517 100644 --- a/awx/ui/client/features/output/stream.service.js +++ b/awx/ui/client/features/output/stream.service.js @@ -4,6 +4,7 @@ import { OUTPUT_MAX_BUFFER_LENGTH, OUTPUT_MAX_LAG, OUTPUT_PAGE_SIZE, + OUTPUT_EVENT_LIMIT, } from './constants'; const rx = []; @@ -42,7 +43,7 @@ function OutputStream ($q) { this.lag = 0; this.chain = $q.resolve(); - this.factors = this.calcFactors(OUTPUT_PAGE_SIZE); + this.factors = this.calcFactors(OUTPUT_EVENT_LIMIT); this.setFramesPerRender(); }; @@ -105,8 +106,7 @@ function OutputStream ($q) { if (missing.length === 0) { ready = this.counters.max; } else if (this.state.overflow) { - const removed = Math.min(missing.length, excess); - ready = missing[removed - 1] - 1; + ready = this.counters.min + this.framesPerRender; } else { ready = missing[0] - 1; }