Skip to content

Commit f9d0271

Browse files
committed
Trace UI Performance Improvements.
The PackageList was taking too long to set up. It turned out the Bacon.combineTemplate streams were eating up multiple seconds (each) to initialize. Added a "SetProp" to create roughly the same functionality, and it seems to be significantly more efficient. (A dataset with 2000 packages went from an 8-10 second load time to a 3-4 second load time) I also had to make a couple of modifications in traces.js since the selection states were converted from Map[id->boolean] to Set[id].
1 parent 69aaa38 commit f9d0271

File tree

4 files changed

+216
-113
lines changed

4 files changed

+216
-113
lines changed

codepulse/src/main/resources/toserve/pages/traces/PackageController.js

Lines changed: 121 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,64 @@
2121

2222
exports.PackageController = PackageController
2323

24+
/*
25+
* Utility class used by the PackageController.
26+
*
27+
* A SetProp instance maintains a `d3.set` of ids. By adding a
28+
* `widget` (e.g. a PackageWidget instance) with a given `id`,
29+
* it will use the `getCurrent` and `getChanges` to make sure
30+
* the "state" of that widget is kept track of in thet set.
31+
*
32+
* This is used as an alternative to `Bacon.combineTemplate`,
33+
* which seems to perform poorly when there are thousands of
34+
* items in the template.
35+
*
36+
* The intended use case for this class is to create a property
37+
* representing the set of all "selected" packages, and another
38+
* property representing the set of all "instrumented" packages.
39+
*
40+
* @param getCurrent - a function(widget) that returns the current
41+
* "state" of the widget, as a boolean
42+
* @param getChanges - a function(widget) that returns a
43+
* Bacon.Observable that pushes boolean events that represent
44+
* the widget's "state" as it changes.
45+
*/
46+
function SetProp(getCurrent, getChanges){
47+
48+
var set = d3.set()
49+
var bus = new Bacon.Bus()
50+
51+
this.prop = bus.toProperty(set)
52+
53+
this.add = function(id, widget){
54+
var current = getCurrent(widget)
55+
if(current) set.add(id)
56+
57+
getChanges(widget).onValue(function(bool){
58+
if(bool & !set.has(id)) {
59+
set.add(id)
60+
bus.push(set)
61+
} else if(!bool && set.has(id)){
62+
set.remove(id)
63+
bus.push(set)
64+
}
65+
})
66+
}
67+
}
68+
2469
function PackageController(treeData, $container, $totalsContainer, $clearSelectionButton){
2570

2671
// build this into a Map[packageId -> packageWidget.selectedProp]
27-
var stateTemplate = {}
72+
var selectedWidgetsSP = new SetProp(
73+
/*getCurrent*/ function(pw){ return pw.selected() },
74+
/*getChanges*/ function(pw){ return pw.selectedProp.changes() }
75+
)
2876

2977
// build this into a Map[packageId -> packageWidget.instrumentationSelectedProp]
30-
var instrumentedTemplate = {}
78+
var instrumentedWidgetsSP = new SetProp(
79+
/*getCurrent*/ function(pw){ return pw.instrumentationSelected() },
80+
/*getChanges*/ function(pw){ return pw.instrumentationSelectedProp.changes() }
81+
)
3182

3283
var widgets = {}
3384

@@ -76,36 +127,63 @@
76127

77128
})(treeData.root, undefined)
78129

130+
var widgetCount = 0
131+
79132
// initialize the `stateTemplate` and `widgets` maps
80133
// based on the package nodes in `treeData`
81134
;(function setupTreeHierarchy(packageParentNode, node){
82135

83136
var pw = new PackageWidget()
84137
widgets[node.id] = pw
138+
pw.associatedNode = node
139+
// pw.parentNode = packageParentNode
140+
widgetCount++
85141

86142
pw.instrumentationSelected(node.partialTraceSelection)
87143

88144
pw.collapseChildren(/* collapsed = */true, /* animate = */false)
89145

90-
stateTemplate[node.id] = pw.selectedProp
91-
pw.selectionClicks.onValue(function(){
92-
handleSelectionClick(node, pw)
93-
})
94-
95-
instrumentedTemplate[node.id] = pw.instrumentationSelectedProp
96-
pw.instrumentationSelectedClicks.onValue(function(){
97-
handleInstrumentationSelectionClick(node, pw)
98-
})
99-
100146
pw.uiParts.collapser.click(function(event){
101147
pw.collapseChildren('toggle', true)
102148
event.stopPropagation()
103149
})
104150

151+
node.children
152+
.sort(function(a,b){
153+
// alphabetic sort by node.label
154+
var an = a.label.toUpperCase(),
155+
bn = b.label.toUpperCase()
156+
157+
if(an < bn) return -1
158+
if(an > bn) return 1
159+
return 0
160+
})
161+
.forEach(function(kid){
162+
var nextParent = (node.kind == 'group' || node.kind == 'package')? node : packageParentNode
163+
setupTreeHierarchy(nextParent, kid)
164+
})
105165

166+
// Figure out the full+abbreviated labels for the widget
106167
if(node.kind == 'root'){
107-
pw.uiParts.main.appendTo($totalsContainer)
108168
pw.abbreviatedLabel('Overall Coverage')
169+
} else if(node.isSelfNode){
170+
pw.fullLabel(node.label).abbreviatedLabel(node.label)
171+
} else {
172+
var abbrevName
173+
174+
if (node.kind != 'group' && packageParentNode && packageParentNode.kind == 'group')
175+
abbrevName = node.label
176+
else {
177+
var parentName = packageParentNode ? packageParentNode.label : '',
178+
abbrevName = node.label.substr(parentName.length)
179+
}
180+
181+
pw.fullLabel(node.label).abbreviatedLabel(abbrevName)
182+
}
183+
184+
if(node.kind == 'root'){
185+
pw.uiParts.main.appendTo($totalsContainer)
186+
// pw.abbreviatedLabel('Overall Coverage')
109187
pw.selectable(false)
110188
pw.instrumentationSelectable(false)
111189

@@ -118,55 +196,45 @@
118196

119197
pw.methodCount(node.methodCount)
120198

199+
200+
121201
if(packageParentNode){
122202
widgets[packageParentNode.id].children.add(pw)
123203
} else {
124204
pw.uiParts.main.appendTo($container)
125205
}
126206

127-
if(node.isSelfNode){
128-
pw.fullLabel(node.label).abbreviatedLabel(node.label)
129-
} else {
130-
var abbrevName
131-
132-
if (node.kind != 'group' && packageParentNode && packageParentNode.kind == 'group')
133-
abbrevName = node.label
134-
else {
135-
var parentName = packageParentNode ? packageParentNode.label : '',
136-
abbrevName = node.label.substr(parentName.length)
137-
}
138-
139-
pw.fullLabel(node.label).abbreviatedLabel(abbrevName)
140-
}
141207
}
142208

143-
node.children
144-
.sort(function(a,b){
145-
// alphabetic sort by node.label
146-
var an = a.label.toUpperCase(),
147-
bn = b.label.toUpperCase()
209+
})(undefined, treeData.root)
148210

149-
if(an < bn) return -1
150-
if(an > bn) return 1
151-
return 0
152-
})
153-
.forEach(function(kid){
154-
var nextParent = (node.kind == 'group' || node.kind == 'package')? node : packageParentNode
155-
setupTreeHierarchy(nextParent, kid)
156-
})
211+
console.log('created', widgetCount, 'PackageWidgets')
157212

158-
})(undefined, treeData.root)
213+
function forEachWidget(f){
214+
for(var id in widgets){
215+
var w = widgets[id], n = w.associatedNode
216+
f(w,n,id)
217+
}
218+
}
219+
220+
forEachWidget(function(pw, node, id){
221+
selectedWidgetsSP.add(id, pw)
222+
instrumentedWidgetsSP.add(id, pw)
223+
pw.selectionClicks.onValue(function(){
224+
handleSelectionClick(node, pw)
225+
})
226+
pw.instrumentationSelectedClicks.onValue(function(){
227+
handleInstrumentationSelectionClick(node, pw)
228+
})
229+
})
159230

160231
// Disable all of the widgets while the trace is running
161232
Trace.running.onValue(function(isRunning){
162-
for(var id in widgets){
163-
var pw = widgets[id],
164-
node = treeData.getNode(id)
165-
233+
forEachWidget(function(pw, node){
166234
if(node.kind != 'root'){
167235
pw.instrumentationSelectable(!isRunning)
168236
}
169-
}
237+
})
170238
})
171239

172240
// checkSelected = function(widget){ return <is widget selected> }
@@ -230,23 +298,22 @@
230298

231299
/**
232300
* Exposes the selection state of each of the widgets, as a
233-
* Map[package.id -> isSelected]
301+
* Set containing the IDs of selected widgets.
234302
*/
235-
this.selectedWidgets = Bacon.combineTemplate(stateTemplate).debounce(10)
303+
this.selectedWidgets = selectedWidgetsSP.prop.debounce(10).noLazy()
236304

237305
/**
238306
* Exposes the 'instrumented' state of each of the widgets, as
239-
* a Map[package.id -> isInstrumented]
307+
* a Set containing the IDs of widgets that are marked as instrumented.
240308
*/
241-
this.instrumentedWidgets = Bacon.combineTemplate(instrumentedTemplate).debounce(10)
309+
this.instrumentedWidgets = instrumentedWidgetsSP.prop.debounce(10).noLazy()
242310

243311
// Decide whether or not to show the "clear all selections" button
244312
// depending on whether or not there are selected widgets
245313
this.selectedWidgets
246-
.map(function(selectionMap){
247-
for(var k in selectionMap) if(selectionMap[k]) return true
314+
.map(function(selectedIds){
315+
return !selectedIds.size()
248316
})
249-
.not()
250317
.assign($clearSelectionButton, 'toggleClass', 'hidden')
251318

252319
// Set all (selectable) widgets to not be selected when the clear selection button is clicked
@@ -282,6 +349,7 @@
282349
this.applyMethodCoverage = function(coverageRecords, activeRecordings){
283350
applyMethodCoverage(treeData, widgets, coverageRecords, activeRecordings, nodePackageParents)
284351
}
352+
285353
}
286354

287355
// Trigger a `flashHighlight` on the appropriate package widgets

codepulse/src/main/resources/toserve/pages/traces/PackageWidget.js

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,46 @@
5858
*/
5959
var packageLabelDefaultWidth = 180
6060

61+
/*
62+
* Creates a function($e, obj) that takes a jQuery element `$e`,
63+
* locates the common UI parts within $e (assuming it is a clone
64+
* of the PackageWidget's template html), and assigns them to
65+
* fields in the `obj`.
66+
*/
67+
var assignUiParts = (function(){
68+
var widgetPartsToFind = [
69+
'childrenContainer',
70+
'labelContainer',
71+
'countContainer',
72+
'barContainer',
73+
'collapser',
74+
'collapserIcon',
75+
'indent',
76+
'contentContainer',
77+
'labelText',
78+
'instrumentationSelectedToggle',
79+
'countNumber',
80+
'barFill',
81+
'barLabel'
82+
]
83+
84+
/*
85+
* Each UI part is located in the template HTML with
86+
* the id "WidgetPart-partName", where `partName`
87+
* corresponds to the string in `widgetPartsToFind`.
88+
*/
89+
var infos = widgetPartsToFind.map(function(name){
90+
var selector = '#WidgetPart-' + name
91+
return {name:name, selector:selector}
92+
})
93+
return function($e, obj){
94+
infos.forEach(function(info){
95+
var $f = $e.find(info.selector)
96+
obj[info.name] = $f
97+
})
98+
}
99+
})()
100+
61101
function PackageWidget(){
62102

63103
// ============================================================================
@@ -87,28 +127,16 @@
87127
// ============================================================================
88128
// UI Selected Elements
89129
// ============================================================================
130+
90131
this.uiParts = {
91132
'main': e,
92133

93-
'childrenContainer': e.find('.widget-children'),
94-
95-
'labelContainer': e.find('.widget-label'),
96-
'countContainer': e.find('.widget-method-count'),
97-
'barContainer': e.find('.widget-barchart'),
98-
'collapser': e.find('.widget-collapser'),
99-
'collapserIcon': e.find('.collapser-icon'),
100-
101-
'indent': e.find('.indent'),
102-
'contentContainer': e.find('.content'),
103-
'labelText': e.find('.label-text'),
104-
'instrumentationSelectedToggle': e.find('.tri-state-toggle'),
105-
'countNumber': e.find('.count-number'),
106-
'barFill': e.find('.bar-fill'),
107-
'barLabel': e.find('.bar-label'),
108-
109134
'highlightsD3': d3.select(e[0]).selectAll('.highlight')
110135
}
111136

137+
// find the remaining UI parts via the helper function `assignUiParts`
138+
assignUiParts(e, this.uiParts)
139+
112140
// ============================================================================
113141
// Public Functions
114142
// ============================================================================
@@ -260,7 +288,7 @@
260288
/*
261289
Exposes the current `selected` state as a Rx Property
262290
*/
263-
this.selectedProp = _selectedBus.toProperty(_selected).skipDuplicates()
291+
this.selectedProp = _selectedBus.toProperty(_selected).skipDuplicates().noLazy()
264292

265293
this.selectionClicks = new Bacon.Bus()
266294

0 commit comments

Comments
 (0)