| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 This in an HTML Import-able file that contains the definition |
| 3 of the following elements: |
| 4 |
| 5 <bot-page-summary> |
| 6 |
| 7 Usage: |
| 8 |
| 9 <bot-page-summary></bot-page-summary> |
| 10 |
| 11 Properties: |
| 12 None. |
| 13 |
| 14 Methods: |
| 15 None. |
| 16 |
| 17 Events: |
| 18 None. |
| 19 --> |
| 20 <link rel="import" href="/res/imp/bower_components/paper-checkbox/paper-checkbox
.html"> |
| 21 |
| 22 <link rel="import" href="/res/imp/common/single-page-style.html"> |
| 23 <link rel="import" href="/res/imp/common/sort-toggle.html"> |
| 24 <link rel="import" href="/res/imp/common/url-param.html"> |
| 25 |
| 26 <link rel="import" href="bot-page-shared-behavior.html"> |
| 27 |
| 28 <dom-module id="bot-page-summary"> |
| 29 <template> |
| 30 <style include="single-page-style"> |
| 31 .wrapper { |
| 32 display: table; |
| 33 margin-left: auto; |
| 34 margin-bottom: 10px; |
| 35 margin-right: 5px; |
| 36 } |
| 37 |
| 38 paper-checkbox { |
| 39 margin-left: 5px; |
| 40 } |
| 41 |
| 42 .thick { |
| 43 border-top-style: solid; |
| 44 } |
| 45 </style> |
| 46 |
| 47 <url-param name="show_full_names" |
| 48 value="{{_show_full_names}}"> |
| 49 </url-param> |
| 50 <url-param name="show_all_tasks" |
| 51 value="{{_show_all_tasks}}"> |
| 52 </url-param> |
| 53 <url-param name="sort_stats" |
| 54 value="{{_sortstr}}" |
| 55 default_value="total:desc"> |
| 56 </url-param> |
| 57 |
| 58 <div class="wrapper"> |
| 59 <table> |
| 60 <thead on-sort_change="_sortChange"> |
| 61 <tr> |
| 62 <th> |
| 63 <span>Name</span> |
| 64 <sort-toggle |
| 65 name="full_name" |
| 66 current="[[_sort]]"> |
| 67 </sort-toggle> |
| 68 </th> |
| 69 <th> |
| 70 <span>Total</span> |
| 71 <sort-toggle |
| 72 name="total" |
| 73 current="[[_sort]]"> |
| 74 </sort-toggle> |
| 75 </th> |
| 76 <th> |
| 77 <span>Success</span> |
| 78 <sort-toggle |
| 79 name="success" |
| 80 current="[[_sort]]"> |
| 81 </sort-toggle> |
| 82 </th> |
| 83 <th> |
| 84 <span>Failed</span> |
| 85 <sort-toggle |
| 86 name="failed" |
| 87 current="[[_sort]]"> |
| 88 </sort-toggle> |
| 89 </th> |
| 90 <th> |
| 91 <span>Died</span> |
| 92 <sort-toggle |
| 93 name="bot_died" |
| 94 current="[[_sort]]"> |
| 95 </sort-toggle> |
| 96 </th> |
| 97 <th> |
| 98 <span>Average Duration</span> |
| 99 <sort-toggle |
| 100 name="avg_duration" |
| 101 current="[[_sort]]"> |
| 102 </sort-toggle> |
| 103 </th> |
| 104 <th> |
| 105 <span>Total Duration</span> |
| 106 <sort-toggle |
| 107 name="total_time" |
| 108 current="[[_sort]]"> |
| 109 </sort-toggle> |
| 110 </th> |
| 111 <th>Percent of Total</th> |
| 112 </tr> |
| 113 </thead> |
| 114 <tbody> |
| 115 <template is="dom-repeat" items="[[_tasksToShow]]" as="task"> |
| 116 <tr> |
| 117 <td hidden$="[[_truthy(_show_full_names)]]" title="[[task.full_nam
e]]">[[task.name]]</td> |
| 118 <td hidden$="[[_not(_show_full_names)]]" title="[[task.full_name]]
">[[task.full_name]]</td> |
| 119 <td>[[task.total]]</td> |
| 120 <td>[[task.success]]</td> |
| 121 <td>[[task.failed]]</td> |
| 122 <td>[[task.bot_died]]</td> |
| 123 <td>[[_humanDuration(task.avg_duration)]]</td> |
| 124 <td>[[_humanDuration(task.total_time)]]</td> |
| 125 <td>[[task.total_time_percent]]%</td> |
| 126 </tr> |
| 127 </template> |
| 128 </tbody> |
| 129 <tr class="thick"> |
| 130 <td>Total</td> |
| 131 <td>[[_totalStats.total]]</td> |
| 132 <td>[[_totalStats.success]]</td> |
| 133 <td>[[_totalStats.failed]]</td> |
| 134 <td>[[_totalStats.bot_died]]</td> |
| 135 <td>[[_humanDuration(_totalStats.avg_duration)]]</td> |
| 136 <td>[[_humanDuration(_totalStats.total_time)]]</td> |
| 137 <td>100.0%</td> |
| 138 </tr> |
| 139 </table> |
| 140 |
| 141 <div> |
| 142 <table> |
| 143 <thead> |
| 144 <tr> |
| 145 <th title="How much time passed between the oldest task fetched an
d now."> |
| 146 Total Wall Time |
| 147 </th> |
| 148 <th title="How much of the wall time this bot was busy with a task
."> |
| 149 Wall Time Utilization |
| 150 </th> |
| 151 </tr> |
| 152 </thead> |
| 153 <tbody> |
| 154 <tr> |
| 155 <td>[[_humanDuration(_totalStats.wall_time)]]</td> |
| 156 <td>[[_totalStats.wall_time_utilization]]%</td> |
| 157 </tr> |
| 158 </tbody> |
| 159 </table> |
| 160 |
| 161 <paper-checkbox |
| 162 checked="{{_show_full_names}}"> |
| 163 Show Full Names |
| 164 </paper-checkbox> |
| 165 <paper-checkbox |
| 166 hidden$="[[_cannotExpand]]" |
| 167 checked="{{_show_all_tasks}}"> |
| 168 Show All Tasks |
| 169 </paper-checkbox> |
| 170 </div> |
| 171 </div> |
| 172 |
| 173 </template> |
| 174 <script> |
| 175 (function(){ |
| 176 var SHOW_LIMIT = 15; |
| 177 Polymer({ |
| 178 is: 'bot-page-summary', |
| 179 |
| 180 behaviors: [ |
| 181 SwarmingBehaviors.BotPageBehavior, |
| 182 ], |
| 183 |
| 184 properties: { |
| 185 // input |
| 186 tasks: { |
| 187 type: Array, |
| 188 }, |
| 189 |
| 190 _cannotExpand: { |
| 191 type: Boolean, |
| 192 computed: "_countTasks(_taskStats.*)" |
| 193 }, |
| 194 _show_all_tasks: { |
| 195 type: Boolean |
| 196 }, |
| 197 _show_full_names: { |
| 198 type: Boolean |
| 199 }, |
| 200 _sortstr: { |
| 201 type: String |
| 202 }, |
| 203 _sort: { |
| 204 type: Object, |
| 205 computed: "_makeSortObject(_sortstr)", |
| 206 }, |
| 207 // _taskStats in an Array<Object> where each object represents one |
| 208 // type of task (e.g. test_windows_release) and aggregate stats |
| 209 // about it (e.g. average duration) |
| 210 _taskStats: { |
| 211 type: Array |
| 212 }, |
| 213 // _tasksToShow is a sorted subset of _taskStats. This allows us to |
| 214 // hide some of the tasks (if there are many) |
| 215 _tasksToShow: { |
| 216 type: Array, |
| 217 computed: "_sortAndLimitTasks(_taskStats.*,_sort.*,_show_all_tasks)" |
| 218 }, |
| 219 // _totalStats contains the aggregate stats for all tasks. |
| 220 _totalStats: { |
| 221 type: Object |
| 222 } |
| 223 }, |
| 224 |
| 225 // We define this down here to listen to all array events (e.g. splices) |
| 226 // otherwise we don't update when more tasks are added. |
| 227 observers: ["_aggregate(tasks.*)"], |
| 228 |
| 229 _aggregate: function() { |
| 230 if (!this.tasks || !this.tasks.length) { |
| 231 return; |
| 232 } |
| 233 // TODO(kjlubick): Fix sk.now() to be less awkward to use. |
| 234 var now = new Date(sk.now() * 1000); |
| 235 var taskNames = []; |
| 236 var taskAgg = {}; |
| 237 var totalStats = { |
| 238 total: this.tasks.length, |
| 239 success: 0, |
| 240 failed: 0, |
| 241 bot_died: 0, |
| 242 avg_duration: 0, |
| 243 total_time: 0, |
| 244 // to compute wall_time, we find the latest task (and assume tasks |
| 245 // come to us chronologically) and find the difference between then |
| 246 // and now. |
| 247 wall_time: (now - this.tasks[this.tasks.length - 1].started_ts) / 1000
, |
| 248 }; |
| 249 this.tasks.forEach(function(t) { |
| 250 var n = t.name.trim(); |
| 251 var pieces = n.split('/'); |
| 252 if (pieces.length === 5) { |
| 253 // this appears to be a buildbot name |
| 254 // piece 0 is tag "name", piece 3 is "buildername" |
| 255 // We throw the rest away (OS, commit hash, build number) so we |
| 256 // can identify the "true name". |
| 257 n = pieces[0] + "/" + pieces[3]; |
| 258 } |
| 259 |
| 260 if (!taskAgg[n]) { |
| 261 taskAgg[n] = { |
| 262 full_name: n, |
| 263 name: n, |
| 264 total: 0, |
| 265 success: 0, |
| 266 failed: 0, |
| 267 bot_died: 0, |
| 268 avg_duration: 0, |
| 269 total_time: 0, |
| 270 } |
| 271 } |
| 272 |
| 273 taskAgg[n].total++; |
| 274 if (t.failure) { |
| 275 totalStats.failed++; |
| 276 taskAgg[n].failed++; |
| 277 } else if (t.internal_failure) { |
| 278 totalStats.bot_died++; |
| 279 taskAgg[n].bot_died++; |
| 280 } else { |
| 281 totalStats.success++; |
| 282 taskAgg[n].success++; |
| 283 } |
| 284 totalStats.total_time += t.duration; |
| 285 taskAgg[n].total_time += t.duration; |
| 286 }); |
| 287 |
| 288 totalStats.avg_duration = totalStats.total_time / totalStats.total; |
| 289 totalStats.wall_time_utilization = (totalStats.total_time * 100 / totalS
tats.wall_time).toFixed(1); |
| 290 this.set("_totalStats", totalStats); |
| 291 |
| 292 // Turn the map into the array and compute total time percent. |
| 293 var names = Object.keys(taskAgg); |
| 294 var taskStats = []; |
| 295 names.forEach(function(n) { |
| 296 taskAgg[n].avg_duration = taskAgg[n].total_time / taskAgg[n].total; |
| 297 taskAgg[n].total_time_percent = (taskAgg[n].total_time * 100 /totalSta
ts.total_time).toFixed(1); |
| 298 taskStats.push(taskAgg[n]); |
| 299 }); |
| 300 |
| 301 // Shorten names if possible by finding the longest common substring |
| 302 // of at least n-1 of the tasks. These parameters can be tweaked; see |
| 303 // https://www.npmjs.com/package/common-substrings for documentation |
| 304 // n-1 seems to be good to avoid not finding decent matches if there |
| 305 // is an oddball task. |
| 306 var substrings = new Substrings({ |
| 307 minOccurrence: Math.max(2, names.length-1), |
| 308 minLength: 6, |
| 309 }); |
| 310 substrings.build(names); |
| 311 var result = substrings.weighByAverage() || []; |
| 312 // result is an Array<{name:String, source:Array<int>} where the |
| 313 // ints in source are the indices of names that have the substring. |
| 314 // result is sorted with the "best" results first. |
| 315 if (result.length) { |
| 316 result[0].source.forEach(function(idx){ |
| 317 var name = taskStats[idx].full_name; |
| 318 taskStats[idx].name = name.replace(result[0].name, "..."); |
| 319 }); |
| 320 } |
| 321 |
| 322 this.set("_taskStats", taskStats); |
| 323 }, |
| 324 |
| 325 _compare: function(a,b) { |
| 326 if (!this._sort) { |
| 327 return 0; |
| 328 } |
| 329 var dir = 1; |
| 330 if (this._sort.direction === "desc") { |
| 331 dir = -1; |
| 332 } |
| 333 return dir * swarming.naturalCompare(a[this._sort.name], b[this._sort.na
me]); |
| 334 }, |
| 335 |
| 336 _countTasks: function(){ |
| 337 return this._taskStats.length <= SHOW_LIMIT; |
| 338 }, |
| 339 |
| 340 _makeSortObject: function(sortstr){ |
| 341 if (!sortstr) { |
| 342 return undefined; |
| 343 } |
| 344 var pieces = sortstr.split(":"); |
| 345 if (pieces.length != 2) { |
| 346 // fail safe |
| 347 return {name: "full_name", direction: "asc"}; |
| 348 } |
| 349 return { |
| 350 name: pieces[0], |
| 351 direction: pieces[1], |
| 352 } |
| 353 }, |
| 354 |
| 355 _sortAndLimitTasks: function() { |
| 356 swarming.stableSort(this._taskStats, this._compare.bind(this)); |
| 357 var limit = this._taskStats.length; |
| 358 if (!this._show_all_tasks && this._taskStats.length > SHOW_LIMIT) { |
| 359 limit = SHOW_LIMIT; |
| 360 } |
| 361 return this._taskStats.slice(0, limit); |
| 362 }, |
| 363 |
| 364 _sortChange: function(e) { |
| 365 // The event we get from sort-toggle tells us the name of what needs |
| 366 // to be sorting and how to sort it. |
| 367 if (!(e && e.detail && e.detail.name)) { |
| 368 return; |
| 369 } |
| 370 e.preventDefault(); |
| 371 e.stopPropagation(); |
| 372 this.set("_sortstr", e.detail.name + ":" + e.detail.direction); |
| 373 }, |
| 374 |
| 375 }); |
| 376 })(); |
| 377 </script> |
| 378 </dom-module> |
| OLD | NEW |