Make rendering long logs faster when rendering huge chunks at once

After change log rendering method, to append HTML elements instead of
rerendering the entire thing, the case of gradual rendering of a log
started to behave much better (because we just append new elements), but
the initial render took a bit longer, because of the fact that appending
large separate HTML elements to DOM at once is not a good idea.

In order to make the situation better I added simple optimization.
Elements are added to DocumentFragment node before inserting to DOM and
appended to DOM only after all elements are processed. That way, when
log needs to be rendered all at once, we will not do any DOM operations
until log is ready.
This commit is contained in:
Piotr Sarnacki 2012-12-05 21:20:58 +01:00
parent e62dc32d2a
commit 27fe85989a
6 changed files with 281 additions and 166 deletions

View File

@ -5,12 +5,7 @@
this should probably be refactored to use container view}}
{{#with view.job.log}}
{{#if id}}
{{#view view.PreView logBinding="view.context.log" logUrlBinding="view.logUrl"}}
<pre id="log" class="ansi"><a href="#" id="tail" {{action toggleTailing target="view"}}>
<span class="status"></span>
<label>Follow logs</label>
</a></pre>
{{/view}}
{{view Travis.PreView logBinding="view.context.log" logUrlBinding="view.logUrl"}}
{{/if}}
{{/with}}

View File

@ -0,0 +1,4 @@
<pre id="log" class="ansi"><a href="#" id="tail" {{action toggleTailing target="view"}}>
<span class="status"></span>
<label>Follow logs</label>
</a></pre>

View File

@ -128,79 +128,111 @@
Travis.app.get('router').urlForEvent(event, repo, item)
).property('job.repo', 'parentView.currentItem')
PreView: Em.View.extend
init: ->
@_super.apply this, arguments
@set 'logManager', Travis.Log.create(target: this)
PreView: Em.View.extend
templateName: 'jobs/pre'
init: ->
@_super.apply this, arguments
@set 'logManager', Travis.Log.create(target: this)
didInsertElement: ->
@_super.apply this, arguments
didInsertElement: ->
@_super.apply this, arguments
Ember.run.next this, ->
if @get 'log.isInitialized'
@logDidChange()
Ember.run.next this, ->
if @get 'log.isInitialized'
@logDidChange()
willDestroy: ->
@get('logManager').destroy()
@get('log.parts').removeArrayObserver this,
willDestroy: ->
@get('logManager').destroy()
@get('log.parts').removeArrayObserver this,
didChange: 'logContentsDidChange'
willChange: 'logContentsWillChange'
version: (->
@rerender()
@set 'logManager', Travis.Log.create(target: this)
).observes('log.version')
logDidChange: (->
if @get('log.isInitialized') && @state == 'inDOM'
@attachLogObservers()
).observes('log', 'log.isInitialized')
attachLogObservers: ->
return if @get('logPartsObserversAttached') == Ember.guidFor(@get('log'))
@set 'logPartsObserversAttached', Ember.guidFor(@get('log'))
Ember.run.next this, ->
@get('logManager').append @get('log.parts')
@get('log.parts').addArrayObserver this,
didChange: 'logContentsDidChange'
willChange: 'logContentsWillChange'
version: (->
@rerender()
@set 'logManager', Travis.Log.create(target: this)
).observes('log.version')
logContentsDidChange: (lines, index, removedCount, addedCount) ->
addedLines = lines.slice(index, index + addedCount)
@get('logManager').append addedLines
logDidChange: (->
if @get('log.isInitialized') && @state == 'inDOM'
@attachLogObservers()
).observes('log', 'log.isInitialized')
logContentsWillChange: (-> )
attachLogObservers: ->
return if @get('logPartsObserversAttached') == Ember.guidFor(@get('log'))
@set 'logPartsObserversAttached', Ember.guidFor(@get('log'))
appendLog: (payloads) ->
url = @get('logUrl')
Ember.run.next this, ->
@get('logManager').append @get('log.parts')
leftOut = []
fragment = document.createDocumentFragment()
@get('log.parts').addArrayObserver this,
didChange: 'logContentsDidChange'
willChange: 'logContentsWillChange'
# TODO: refactor this loop, it's getting messy
for payload in payloads
line = payload.content
number = payload.number
logContentsDidChange: (lines, index, removedCount, addedCount) ->
addedLines = lines.slice(index, index + addedCount)
@get('logManager').append addedLines
unless payload.append
pathWithNumber = "#{url}#L#{number}"
p = document.createElement('p')
p.innerHTML = '<a href="%@" id="L%@" class="log-line-number" name="L%@">%@</a>%@'.fmt(pathWithNumber, number, number, number, line)
line = p
logContentsWillChange: (-> )
if payload.fold && !payload.foldContinuation
div = document.createElement('div')
div.appendChild line
div.className = "fold #{payload.fold} show-first-line"
line = div
appendLog: (payloads) ->
url = @get('logUrl')
for payload in payloads
line = payload.content
number = payload.number
unless payload.append
pathWithNumber = "#{url}#L#{number}"
line = '<p><a href="%@" id="L%@" class="log-line-number" name="L%@">%@</a>%@</p>'.fmt(pathWithNumber, number, number, number, line)
if payload.fold && !payload.foldContinuation
line = "<div class='fold #{payload.fold} show-first-line'>#{line}</div>"
if payload.replace
this.$("#log #L#{number}").parent().replaceWith line
else if payload.append
this.$("#log #L#{number}").parent().append line
else if payload.foldContinuation
this.$("#log .fold.#{payload.fold}:last").append line
if payload.replace
if link = fragment.querySelector("#L#{number}")
link.parentElement.innerHTML = line.innerHTML
else
this.$('#log').append(line)
this.$("#L#{number}").parent().replaceWith line
else if payload.append
if link = fragment.querySelector("#L#{number}")
link.parentElement.innerHTML += line
else
this.$("#L#{number}").parent().append line
else if payload.foldContinuation
folds = fragment.querySelectorAll(".fold.#{payload.fold}")
if fold = folds[folds.length - 1]
fold.appendChild line
else
this.$("#log .fold.#{payload.fold}:last").append line
else
fragment.appendChild(line)
if payload.openFold
this.$("#log .fold.#{payload.openFold}:last").
removeClass('show-first-line').
addClass('open')
if payload.openFold
folds = fragment.querySelectorAll(".fold.#{payload.fold}")
if fold = folds[folds.length - 1]
fold = $(fold)
else
fold = this.$(".fold.#{payload.fold}:last")
if payload.foldEnd
this.$("#log .fold.#{payload.fold}:last").removeClass('show-first-line')
fold.removeClass('show-first-line').addClass('open')
if payload.foldEnd
folds = fragment.querySelectorAll(".fold.#{payload.fold}")
if fold = folds[folds.length - 1]
fold = $(fold)
else
fold = this.$(".fold.#{payload.fold}:last")
fold.removeClass('show-first-line')
this.$('#log')[0].appendChild fragment

View File

@ -76,8 +76,8 @@ describe 'Travis.Log', ->
{ number: 8, fold: 'qux', foldContinuation: true },
{ number: 9, fold: 'qux', foldContinuation: true, foldEnd: true }]
expect( lines[4] ).toEqual ['$ end', '']
expect( options[4]).toEqual [{ number: 10 }, { number: 11 }]
expect( lines[4] ).toEqual ['$ end']
expect( options[4]).toEqual [{ number: 10 }]
it 'works properly when log is started with fold', ->
fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
@ -97,8 +97,8 @@ describe 'Travis.Log', ->
{ number: 1, fold: 'foo' },
{ number: 2, fold: 'foo', foldContinuation: true, foldEnd: true }]
expect( lines[1] ).toEqual ['$ bar', '']
expect( options[1]).toEqual [{ number: 3 }, { number: 4 }]
expect( lines[1] ).toEqual ['$ bar']
expect( options[1]).toEqual [{ number: 3 }]
it 'works properly for 2 consecutive folds', ->
fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
@ -124,8 +124,8 @@ describe 'Travis.Log', ->
{ number: 3, fold: 'foo' },
{ number: 4, fold: 'foo', foldContinuation: true, foldEnd: true }]
expect( lines[2] ).toEqual ['$ bar', '']
expect( options[2]).toEqual [{ number: 5 }, { number: 6 }]
expect( lines[2] ).toEqual ['$ bar']
expect( options[2]).toEqual [{ number: 5 }]
it 'works fine with not finalized fold', ->
fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
@ -139,11 +139,10 @@ describe 'Travis.Log', ->
lines = target.get('calls').map (call) -> call.lines
options = target.get('calls').map (call) -> call.options
expect( lines[0] ).toEqual ['$ foo --foo', '1', '']
expect( lines[0] ).toEqual ['$ foo --foo', '1']
expect( options[0]).toEqual [
{ fold: 'foo', number: 1 },
{ fold: 'foo', number: 2, foldContinuation: true },
{ fold: 'foo', number: 3, foldContinuation: true }]
{ fold: 'foo', number: 2, foldContinuation: true }]
it 'allows to continue fold', ->
fold = Em.Object.create name: 'foo', startPattern: /^\$ foo/, endPattern: /^\$/
@ -164,25 +163,23 @@ describe 'Travis.Log', ->
lines = target.get('calls').map (call) -> call.lines
options = target.get('calls').map (call) -> call.options
expect( lines[0] ).toEqual ['$ foo --foo', '1', '']
expect( lines[0] ).toEqual ['$ foo --foo', '1']
expect( options[0]).toEqual [
{ fold: 'foo', number: 1 },
{ fold: 'foo', number: 2, foldContinuation: true },
{ fold: 'foo', number: 3, foldContinuation: true }]
{ fold: 'foo', number: 2, foldContinuation: true }]
expect( lines[1] ).toEqual ['2', '']
expect( lines[1] ).toEqual ['2']
expect( options[1]).toEqual [
{ fold: 'foo', number: 3, foldContinuation: true, append: true }
{ fold: 'foo', number: 4, foldContinuation: true }
{ fold: 'foo', number: 3, foldContinuation: true }
]
expect( lines[2] ).toEqual ['3']
expect( options[2]).toEqual [
{ fold: 'foo', number: 4, foldContinuation: true, append: true, foldEnd: true }
{ fold: 'foo', number: 4, foldContinuation: true, foldEnd: true }
]
expect( lines[3] ).toEqual ['$ bar', '']
expect( options[3]).toEqual [{ number: 5 }, { number: 6 }]
expect( lines[3] ).toEqual ['$ bar']
expect( options[3]).toEqual [{ number: 5 }]
it 'notifies that the line should be appended', ->
log.append '$ foo\n.'
@ -201,8 +198,8 @@ describe 'Travis.Log', ->
expect( lines[1] ).toEqual ['...']
expect( options[1]).toEqual [{ append: true, number: 2 }]
expect( lines[2] ).toEqual ['..', '$ bar', '']
expect( options[2]).toEqual [{ append: true, number: 2 }, { number: 3 }, { number: 4 }]
expect( lines[2] ).toEqual ['..', '$ bar']
expect( options[2]).toEqual [{ append: true, number: 2 }, { number: 3 }]
it 'notifies that the line should be replaced', ->
log.append '$ foo\n'
@ -216,17 +213,17 @@ describe 'Travis.Log', ->
lines = target.get('calls').map (call) -> call.lines
options = target.get('calls').map (call) -> call.options
expect( lines[0] ).toEqual ['$ foo', '']
expect( options[0]).toEqual [{ number: 1 }, { number: 2 }]
expect( lines[0] ).toEqual ['$ foo']
expect( options[0]).toEqual [{ number: 1 }]
expect( lines[1] ).toEqual ['', 'Downloading 50%']
expect( options[1]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }]
expect( options[1]).toEqual [{ number: 2 }, { number: 2, replace: true }]
expect( lines[2] ).toEqual ['', 'Downloading 100%', '']
expect( options[2]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }, { number: 3 }]
expect( lines[2] ).toEqual ['', 'Downloading 100%']
expect( options[2]).toEqual [{ number: 2, append: true }, { number: 2, replace: true }]
expect( lines[3] ).toEqual ['$ bar', '']
expect( options[3]).toEqual [{ number: 3, append: true }, { number: 4 }]
expect( lines[3] ).toEqual ['$ bar']
expect( options[3]).toEqual [{ number: 3 }]
it 'notifies that the line should be replaced even if carriage return is in the middle', ->

View File

@ -0,0 +1,161 @@
view = null
store = null
record = null
describe 'Travis.PreView', ->
beforeEach ->
store = Travis.Store.create()
afterEach ->
store.destroy()
view.remove()
view.destroy()
it 'works fine with existing log, which is appended', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' }
log = Travis.Artifact.find(1)
log.set('version', 1)
Ember.run ->
view = Travis.PreView.create(log: null)
view.append()
expect( view.$('#log').length ).toEqual 1
Ember.run ->
view.set 'log', log
log.set 'isLoaded', true
waits 50
runs ->
expect( view.$('#log p').length ).toEqual 1
expect( view.$('#log p').text().trim() ).toEqual '1$ start'
Ember.run ->
log.append('$ end')
waits 50
runs ->
expect( view.$('#log p').length ).toEqual 2
expect( view.$('#log p').text().trim() ).toEqual '1$ start2$ end'
it 'works fine with log already attahed to view', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' }
log = Travis.Artifact.find(1)
Ember.run ->
view = Travis.PreView.create()
view.set('log', log)
view.append()
Ember.run ->
log.append('end')
waits 50
runs ->
expect( view.$('#log p').length ).toEqual 2
expect( view.$('#log p').text().trim() ).toEqual '1$ start2end'
it 'folds items', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start\n' }
log = Travis.Artifact.find(1)
Ember.run ->
view = Travis.PreView.create()
view.set('log', log)
view.append()
Ember.run ->
log.append '$ bundle install\n1\n2\n'
Ember.run ->
log.append '3\n4\n$ something'
waits 50
runs ->
expect( view.$('#log > p').length ).toEqual 2
expect( view.$('#log .fold.bundle').length ).toEqual 1
expect( view.$('#log .fold.bundle > p').length ).toEqual 5
it 'works properly with fragment document', ->
store.load Travis.Artifact, 1, { id: 1, body: '' }
log = Travis.Artifact.find(1)
Ember.run ->
view = Travis.PreView.create()
view.set('log', log)
view.append()
waits 50
runs ->
payloads = [
{ number: 1, content: 'foo' }
{ number: 1, content: 'bar', append: true }
]
# it should work even if we need to append to fragment in memory
view.appendLog(payloads)
expect( view.$('#L1').parent().text().trim() ).toEqual '1foobar'
# now, let's append more to this line, it's in DOM already
view.appendLog([ { number: 1, content: 'baz', append: true } ])
expect( view.$('#L1').parent().text().trim() ).toEqual '1foobarbaz'
payloads = [
{ number: 1, content: 'foo', replace: true }
]
# replace should work in DOM
view.appendLog(payloads)
expect( view.$('#L1').parent().text().trim() ).toEqual '1foo'
payloads = [
{ number: 2, content: 'foo' }
{ number: 2, content: 'bar', replace: true }
]
# replace should work when element is in fragment
view.appendLog(payloads)
expect( view.$('#L2').parent().text().trim() ).toEqual '2bar'
payloads = [
{ number: 3, content: '$ bundle install', fold: 'bundle' }
{ number: 4, content: 'Installing rails', fold: 'bundle', foldContinuation: true }
]
# folds should work properly with fragment
view.appendLog(payloads)
expect( view.$('.bundle #L3').parent().text().trim() ).toEqual '3$ bundle install'
expect( view.$('.bundle #L4').parent().text().trim() ).toEqual '4Installing rails'
expect( view.$('.bundle > p').length ).toEqual 2
payloads = [
{ number: 5, content: 'Installing travis', fold: 'bundle', foldContinuation: true }
]
# folds should also work when already in DOM
view.appendLog(payloads)
expect( view.$('.bundle #L5').parent().text().trim() ).toEqual '5Installing travis'
expect( view.$('.bundle > p').length ).toEqual 3
# regular line append
view.appendLog([ { number: 6, content: 'next'} ])
expect( view.$('#L6').parent().text().trim() ).toEqual '6next'
# openFold when in fragment
payloads = [
{ number: 7, content: '$ install', fold: 'install' }
{ number: 8, content: 'Installing foo', fold: 'install', foldContinuation: true }
{ number: 9, content: 'error', openFold: true, fold: 'install', foldContinuation: true }
]
# folds should work properly with fragment
view.appendLog(payloads)
expect( view.$('.install').hasClass('show-first-line') ).toEqual false
# end fold when in fragment
payloads = [
{ number: 10, content: '$ install', fold: 'install2' }
{ number: 11, content: 'Installing foo', fold: 'install2', foldEnd: true, foldContinuation: true }
]
# folds should work properly with fragment
view.appendLog(payloads)
expect( view.$('.install2').hasClass('show-first-line') ).toEqual false

View File

@ -1,74 +0,0 @@
view = null
store = null
record = null
describe 'Travis.LogView', ->
beforeEach ->
store = Travis.Store.create()
afterEach ->
store.destroy()
view.remove()
view.destroy()
it 'works fine with existing log, which is appended', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
log = Travis.Artifact.find(1)
Ember.run ->
view = Travis.LogView.create(context: null)
view.append()
expect( $('#log').length ).toEqual 1
console.log $('#log')
job = Ember.Object.create log: log, subscribe: (-> )
Ember.run ->
view.set 'context', job
log.set 'isLoaded', true
expect( view.$('#log p').length ).toEqual 1
expect( view.$('#log p').text().trim() ).toEqual '1$ start'
Ember.run ->
log.append('$ end')
expect( view.$('#log p').length ).toEqual 2
expect( view.$('#log p').text().trim() ).toEqual '1$ start2$ end'
it 'works fine with log already attahed to view', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
log = Travis.Artifact.find(1)
job = Ember.Object.create log: log, subscribe: (-> )
Ember.run ->
view = Travis.LogView.create()
view.set('context', job)
view.append()
Ember.run ->
log.append('end')
expect( view.$('#log p').length ).toEqual 2
expect( view.$('#log p').text().trim() ).toEqual '1$ start2end'
it 'folds items', ->
store.load Travis.Artifact, 1, { id: 1, body: '$ start' }
log = Travis.Artifact.find(1)
job = Ember.Object.create log: log, subscribe: (-> )
Ember.run ->
view = Travis.LogView.create()
view.set('context', job)
view.append()
Ember.run ->
log.append '$ bundle install\n1\n2'
Ember.run ->
log.append '3\n4\n$ something'
expect( view.$('#log > p').length ).toEqual 2
expect( view.$('#log .fold.bundle').length ).toEqual 1
expect( view.$('#log .fold.bundle > p').length ).toEqual 5